История compact() и extract()

Источник: «A story of compact() and extract()»
compact() и extract() — две классические функции PHP: давайте рассмотрим, как они используются сейчас и как их можно модернизировать.

Оглавление

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. Тем не менее есть несколько опций, позволяющих изменить это поведение.

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() часто используется внутри метода. Это означает, что создание переменных происходит в локальном контексте, а на глобальную область видимости это оказывает ограниченное влияние, если только нет явного глобального вызова.

<?php

function foo(array $data) {
extract($data);

// дополнительные инструкции по обработке
}

?>

extract() почти всегда используется с параметрами по умолчанию

Поэтому чаще всего extract() используется только с одним аргументом. Второй аргумент — это опция, определяющая, как функция будет реагировать при обнаружении существующей переменной. Вот опции и как часто их использовали:

* EXTR_OVERWRITE — это значение по умолчанию: оно перезаписывает все существующие значения новыми. Это также наиболее часто используемое значение. Даже EXTR_SKIP, пропускающий существующие значения, используется почти в половине проектов. Как правило, extract() используется для перезаписи существующих переменных или для дополнения набора существующих переменных.

Одна из очень интересных опций, которая редко используется, — EXTR_IF_EXISTS: эта опция только перезаписывает существующие переменные. Это означает, что новая переменная не создаётся. Функция сначала создаст ожидаемые переменные со значением по умолчанию. Позже они будут обновлены входящим массивом, когда он будет доступен.

extract() работает с обобщёнными значениями

Другой аспект — это название контейнеров данных, которые используются в extract(). Они всегда очень обобщённые. Извлекаются $parameters, $data, $this->INI

Их форма разнообразна: свойства, параметры, переменные с возвращаемыми значениями. Но их название редко бывает более точным. Это сочетается с небольшим количеством проверок: код имеет высокий уровень доверия к источнику.

Как 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() и сделать его более безопасным.

Дополнительные материалы

Предыдущая Статья

Простое управление временными файлами в Laravel

Следующая Статья

Создание изолируемых команд в Laravel