История compact() и extract()
compact()
и extract()
— две классические функции PHP: давайте рассмотрим, как они используются сейчас и как их можно модернизировать.Оглавление
- Из переменных в массив и обратно
- Альтернативный синтаксис для кода compact() и extract()
- Другой альтернативный синтаксис для compact() и extract()
- Использование compact() и extract()
- Случай для extract()
- Возможные будущие обновления compact() и extract()
compact()
и extract()
— две стороны одной медали. Они также являются неотъемлемой частью истории PHP, как и их близкие родственники — переменные переменных. Давайте рассмотрим использование compact()
и extract()
и посмотрим, как они могут войти в будущее PHP.
Из переменных в массив и обратно
compact()
принимает список имён переменных в виде строк и создаёт массив, ключами которого являются имена переменных, а соответствующими значениями — значения переменных. extract()
выполняет обратную операцию и создаёт переменные из массива, состоящего из пар name => value
.
<?php
$a = 1;
$array = compact('a');
// $array === ['a' => 1];
$array['a'] = 3;
$array['b'] = 2;
extract($array);
echo $a; // 3
echo $b; // 2
?>
Как показано выше, compact()
считывает имя переменной, затем переменную и её значение, после чего создаёт массив. Это никак не влияет на локальные переменные, которые после этого процесса остаются прежними.
С другой стороны, extract()
влияет на локальный контекст, обновляя, по умолчанию, или создавая переменные. Это приводит к совершенно разным последствиям.
Альтернативный синтаксис для кода compact() и extract()
compact()
можно эмулировать с помощью знаменитых переменных переменных PHP. Переменные переменных легко заметить в коде по двойному (или более) $$
в имени. Первая (внутренняя) переменная используется для динамического задания имени второй переменной. Затем из неё извлекается её значение.
<?php
$a = 1;
$list =['a'];
// подобие compact()
$compact = [];
foreach($list as $variable) {
$compact[$variable] = $$variable;
}
// подобие extract()
foreach($compact as $name => $value) {
// создаёт переменную, с именем $name и значением $value
$$name = $value;
}
?>
Это хорошо иллюстрирует важную роль compact()
и extract()
. Они являются связующим звеном между миром переменных и миром данных. Данные хранятся в строках (по крайней мере, здесь), а манипуляции с ними осуществляются через переменные. Имена переменных обычно жёстко закодированы в синтаксисе PHP. С помощью переменных переменных, compact()
и extract()
, можно переходить от переменных к массивам и обратно.
Другой альтернативный синтаксис для compact() и extract()
Другой альтернативой коду compact()
и extract()
является использование сигнатур и параметров функций. На самом деле они имеют очень похожие возможности и несколько ключевых отличий.
Можно превратить массив значений в набор переменных, вызвав другой метод с помощью оператора spread ...
. В PHP 8.0 и более поздних версиях именованные параметры соответствуют имени индекса с параметром. Позже, внутри метода, это будут реальные переменные.
<?php
function foo($a, $b, $c) {
print "$a $b $c";
}
$args = ['a' => 1, 'c' => 3, 'b' => 2, ];
foo(...$args); // 1 2 3
ksort($args);
foo(...array_values($args)); // 1 2 3
?>
В данном случае оператор spread в сочетании с именованными параметрами выступает в роли функции extract()
. Это может быть более очевидным при вызове функции call_user_func_array()
.
Обратите внимание, что эта альтернатива позволяет просто добавить проверку типа и проверку имени: они заложены в сигнатуру метода. Запрет лишних параметров также обрабатывается с помощью фатальной ошибки Unknown named parameter
.
Эквивалентом функции compact()
является функция get_defined_vars()
, которая перечисляет локальные переменные. В зависимости от локального контекста и использования, она может выполнять ту же роль.
<?php
function foo() {
$a = 1;
$b = get_defined_vars();
print_r($b);
}
foo();
// ['a' => 1];
// Здесь нет $b, поскольку она присваивается ПОСЛЕ get_defined_vars()
?>
Использование compact() и extract()
Теперь давайте посмотрим на реальное использование compact()
и extract()
. Из 3000+ проектов с открытым исходным кодом они используются соответственно в 403 и 390 проектах.
Поскольку эти две функции должны быть противоположны друг другу, можно было бы ожидать, что они будут использоваться в равных количествах. Это почти так, но не совсем. Если говорить о деталях, то 257 проектов используют обе функции, а остальные — только одну из них.
Другой аспект их использования — применение опций для extract()
. По умолчанию функция перезаписывает локальные переменные теми, что находятся во входящем массиве: это EXTR_OVERWRITE
. Тем не менее есть несколько опций, позволяющих изменить это поведение.
EXTR_SKIP
: 148 проектов используют эту опциюEXTR_OVERWRITE
: 57 проектов используют эту опцию явноEXTR_PREFIX_SAME
: 11 проектовEXTR_PREFIX_ALL
: 9 проектовEXTR_PREFIX_INVALID
: 3 проектаEXTR_IF_EXISTS
: 3 проектаEXTR_PREFIX_IF_EXISTS
: 1 проект
extract()
предлагает множество различных вариантов поведения, но наиболее часто используемым является стандартный и наиболее очевидный: перезапись локальных переменных входящими значениями.
Перезапись может стать проблемой, когда переменные заменяются неконтролируемыми значениями. Это делает extract()
угрозой безопасности, так как открывает возможность изменения поведения текущего кода.
Обратите внимание, что compact()
не подвержен этой проблеме.
Теперь рассмотрим compact()
и extract()
по отдельности.
Некоторые функции требуют массив с несколькими значениями
Функция compact()
полезна, когда необходимо передать или вернуть несколько значений из другого метода. Один из классических подходов — поместить их в массив. Это относится к параметрам опции. Например, нативная функция session_start()
ожидает несколько параметров.
<?php
$cookie_lifetime = $config->session->timetolive;
session_start(compact('cookie_lifetime'));
// эквивалентно
session_start(['cookie_lifetime' => 86400, ]);
?>
Когда параметры приходится собирать из нескольких источников, удобно помещать их в хорошо именованные переменные, а затем, в последний момент, compact()
их.
Существует множество кастомных функций из самого PHP, а также различных фреймворков и CMS, работающих подобным образом. Структура массива сокращает количество аргументов до одного и делает параметры необязательными: их можно опустить.
Обратите внимание, что такое поведение очень похоже на использование большого количества параметров и именованных параметров. Оператор spread фактически выполняет операцию извлечения.
<?php
function foo(array $config) {}
foo(compact('a', 'b', 'c'));
// эквивалентно
function goo(string $a, int $b, bool $c) {}
goo(...compact('a', 'b', 'c'));
?>
Управление переменными после длинного метода
Подход compact()
также является решением для модернизации legacy кода. Когда большой метод создаёт множество локальных переменных, которые трудно отделить одну от другой, может быть проще собрать нужные значения в конце метода, в массив, и оставить предыдущий код нетронутым.
<?php
function longMethod() {
// Представьте много кода
...
...
// перед финальным возвратом
return compact('user', 'name', 'family', 'address', 'zip_code');
}
?>
Передача переменных в свойства
compact()
создаёт массив значений. В современном PHP объект был бы ещё одним приемлемым вариантом с точки зрения скорости и использования памяти.
Ближайший вариант — использовать класс stdClass
через приведение (object)
. Учитывая, что переменные и свойства имеют схожие ограничения на именование, это простой шаг, если не более производительный.
<?php
$object = (object) compact('a', 'b', 'c');
$myObject = new MyClass(...compact('a', 'b', 'c'));
$myObject = MyClass::createFromArray(compact('a', 'b', 'c'));
?>
Чтобы выйти за рамки stdClass
, нужен полноценный класс с конструктором. Это требует большего количества кода, но в конечном итоге использовать его так же просто, как и оператор приведения.
Случай для extract()
Теперь давайте посмотрим на extract()
. Это совсем другой зверь, поскольку он пишет в локальный контекст. extract()
используется для расширения списка значений в переменные. В этом он противоположен функции compact()
.
При просмотре реального использования кода в проектах исходный текст выглядит следующим образом: имена переменных должны сказать вам достаточно, чтобы понять это.
<?php
class x {
private $parameters = array();
private array $INI;
function foo(array $data) {
extract($_POST);
extract($data);
$db_row = fetchDataInDatabase();
extract($db_row);
extract($this->parameters);
extract($this->INI);
}
}
?>
Три аспекта этого кода представляют интерес:
extract()
используется внутри методаextract()
чаще всего используется с параметрами по умолчаниюextract()
работает с обобщёнными значениями
extract() часто используется внутри метода
extract()
часто используется внутри метода. Это означает, что создание переменных происходит в локальном контексте, а на глобальную область видимости это оказывает ограниченное влияние, если только нет явного глобального вызова.
<?php
function foo(array $data) {
extract($data);
// дополнительные инструкции по обработке
}
?>
extract() почти всегда используется с параметрами по умолчанию
Поэтому чаще всего extract()
используется только с одним аргументом. Второй аргумент — это опция, определяющая, как функция будет реагировать при обнаружении существующей переменной. Вот опции и как часто их использовали:
EXTR_OVERWRITE
: 53 проекта *EXTR_SKIP
: 143 проектаEXTR_PREFIX_SAME
: 11 проектовEXTR_PREFIX_ALL
: 9 проектовEXTR_IF_EXISTS
: 3 проектаEXTR_PREFIX_IF_EXISTS
: 1 проект
* EXTR_OVERWRITE
— это значение по умолчанию: оно перезаписывает все существующие значения новыми. Это также наиболее часто используемое значение. Даже EXTR_SKIP
, пропускающий существующие значения, используется почти в половине проектов. Как правило, extract()
используется для перезаписи существующих переменных или для дополнения набора существующих переменных.
Одна из очень интересных опций, которая редко используется, — EXTR_IF_EXISTS
: эта опция только перезаписывает существующие переменные. Это означает, что новая переменная не создаётся. Функция сначала создаст ожидаемые переменные со значением по умолчанию. Позже они будут обновлены входящим массивом, когда он будет доступен.
extract() работает с обобщёнными значениями
Другой аспект — это название контейнеров данных, которые используются в extract()
. Они всегда очень обобщённые. Извлекаются $parameters
, $data
, $this->INI
…
Их форма разнообразна: свойства, параметры, переменные с возвращаемыми значениями. Но их название редко бывает более точным. Это сочетается с небольшим количеством проверок: код имеет высокий уровень доверия к источнику.
$_POST
, или его кузены$_GET
,$_REQUEST
и т. д., — это смутный сувенир отregister_global
. По сути, он сбрасывает входящие переменные из HTML-формы в текущий контекст. Это очень старый способ программирования, небезопасный. Такие способы встречаются редко.$this->INI
— это случай с конфигурацией. Значения конфигураций хранятся в файле.ini
(или.yaml
, или.toml
…). Чтобы с ними было удобнее работать, чем с синтаксисом$INI['configuration_name']
, они превращаются в переменные. Самое главное, что список конфигурационных переменных в INI-файле нигде не документируется. Он должен быть гибким и принимать всё что угодно, поскольку конфигурации часто меняются по количеству и форме.$data
и$parameters
часто используются в шаблонах представления. Контроллер собирает все необходимые данные, передающиеся как одна группа в класс представления. Чтобы сохранить простоту шаблона представления, входящие параметры извлекаются в виде списка переменных. Опять, в этой ситуации извлечение мало что знает о том, какие переменные предоставляются контроллером, а какие нужны представлению. И те, и другие распоряжаются ими по своему усмотрению, и это неподвластноextract()
.$db_row
— это вариация вышеописанной системы шаблонов. Строка значений извлекается из базы данных, а затем превращается в список локальных переменных. На этот раз список переменных известен, как список столбцов в SQL-командеSELECT
; однако зачастую он произвольно определяется автором SQL-команды, а не принимающей стороной.
Как foreach() с list()
Мы закончим этот обзор функцией, похожей на extract()
, которая работает в foreach()
. Можно использовать list()
(он же []
), чтобы превратить значения в набор переменных. В дальнейшем это избавит вас от синтаксиса массивов в цикле.
<?php
foreach($db_set as $row) {
print $row['a'].' '.$row['b'].PHP_EOL;
}
// Аналогично вышеописанной
foreach($db_set as ['a' => $a, 'b' => $b]) {
print $a . ' ' . $b . PHP_EOL;
}
// Также существует в позиционном виде
foreach($db_set as [$a, $b]) {
print $a . ' ' . $b . PHP_EOL;
}
?>
Случай с неизвестными извлечёнными переменными
Самый распространённый случай использования extract()
— это разделение в коде. Один оператор должен определить произвольное количество значений, каждое из которых имеет имя. Это подразумевает файлы конфигураций, табличные наборы данных (подумайте о SQL-строках, но в любом стиле электронных таблиц), системы шаблонов.
Затем эта масса значений, где и имена значений, и сами значения произвольны, передаётся в центральную систему для обработки. В том же порядке, что и раньше, парсер конфигурации, коннектор базы данных или система представления.
Наконец, для удобства всё эти переменные доступны в коде.
Такой подход объясняет, почему extract()
настроен на перезапись существующих переменных: входящие значения имеют приоритет над значениями по умолчанию. Удобнее перезаписать переменные, чем проверять, существуют ли они.
Лишние значения обычно игнорируются, поскольку код их не использует. Подобная практика вызывает опасения в плане безопасности, поскольку злоумышленник может найти способ нарушить контекст или обновить важную переменную.
Лишние значения, вероятно, лучше обрабатывать с помощью опции EXTR_IF_EXISTS
: она перезаписывает переменные только в том случае, если они существуют, и полностью игнорирует несуществующие. Нет значения по умолчанию, нет extract()
— это девиз.
Возможные будущие обновления compact() и extract()
extract()
выглядит как реликт из прошлого, пришедший из эпохи register_global
. Но на самом деле она служит конкретной цели: когда необходимо манипулировать большим количеством значений по имени, но при этом полностью определяться фрагментами кода, которые не являются теми, кто выполняет извлечение.
Обработка данных из формы, чтение длинных конфигураций, предоставление большого количества данных представлениям и извлечение строк из баз данных — вот все возможные варианты использования. Это классическая функция многих PHP-приложений.
Тем не менее можно дать несколько рекомендаций по использованию extract()
и сделать его более безопасным.
- Избегайте использования
extract()
вне метода. Глобальный контекст также является хранилищем всех глобальных значений, которые могут повлиять на любую другую часть приложения. Поэтому безопаснее всего держатьextract()
в методе, с хорошо контролируемым количеством переменных в локальной области видимости и малой вероятностью переполнения. - Обновите параметр по умолчанию на
EXTR_IF_EXISTS
, вместоEXTR_OVERWRITE
. Это означает, что входящие переменные должны быть инициализированы перед обновлением черезextract()
. Нет значения по умолчанию — нетextract()
. Это похоже на инициализацию переменных в других местах PHP: присвойте им значение по умолчанию, для инициализации, а позже измените их. - Рассмотрите возможность использования сигнатуры метода вместо
extract()
для публикации большого количества переменных. Современный PHP умеет использовать...
три точки для вызова метода с набором аргументов в массиве. В сигнатуре метода указываются переменные с типом и значением по умолчанию. По сути, это работает какextract()
, но с расширенными мерами безопасности. - Рассмотрите возможность переноса этого массива в объект. Преобразование, за неимением лучшего слова, массива в объект и его свойства поможет держать значения под контролем. В частности, свойства могут иметь значение по умолчанию и тип, который отсутствует в
extract()
. После этого объект может легко передавать значения остальным частям приложения.