Вызов волшебного метода PHP 5.3
Эта тема расширяется на Когда я должен/должен использовать __construct(), __get(), __set() и __call() в PHP?, в котором говорится о __construct
, __get
и __set
магические методы.
Начиная с PHP 5.3 существует новый магический метод под названием __invoke
. Метод __invoke
вызывается, когда скрипт пытается вызвать объект как функцию.
Теперь, когда я провел исследование для этого метода, люди сравнивают его с методом Java .run()
- см. Интерфейс, доступный для запуска.
Подумав долго и упорно размышляя об этом, я не могу придумать ни одной причины, по которой вы бы позвонили $obj();
в отличие от $obj->function();
Даже если бы вы перебирали массив объектов, вы все равно знали бы имя основной функции, которую хотели бы запустить.
Так является ли волшебный метод __invoke
еще одним примером ярлыка "просто потому, что вы можете, это не значит, что вы должны" в PHP, или есть случаи, когда это действительно было бы правильно?
6 answers
PHP не позволяет передавать указатели функций, как в других языках. Функции не первого класса в PHP. Функции первого класса в основном означают, что вы можете сохранить функцию в переменной, передать ее и выполнить в любое время.
Метод __invoke
- это способ, которым PHP может использовать псевдо-первоклассные функции.
Метод __invoke
может использоваться для передачи класса, который может действовать как замыкание или продолжение , или просто как функция, которую вы можете передавать по кругу.
Многие функции функционального программирования основаны на функциях первого класса. Даже обычное императивное программирование может извлечь из этого выгоду.
Допустим, у вас была процедура сортировки, но вы хотели поддерживать различные функции сравнения. Ну, у вас могут быть разные классы сравнения, которые реализуют функцию __invoke и передают экземпляры в класс вашей функции сортировки, и ей даже не нужно знать имя функции.
Действительно, ты всегда мог бы сделали что-то вроде передачи класса и заставили функцию вызывать метод, но теперь вы почти можете говорить о передаче "функции" вместо передачи класса, хотя это не так чисто, как в других языках.
Использование __invoke
имеет смысл, когда вам нужен вызываемый , который должен поддерживать некоторое внутреннее состояние. Допустим, вы хотите отсортировать следующий массив:
$arr = [
['key' => 3, 'value' => 10, 'weight' => 100],
['key' => 5, 'value' => 10, 'weight' => 50],
['key' => 2, 'value' => 3, 'weight' => 0],
['key' => 4, 'value' => 2, 'weight' => 400],
['key' => 1, 'value' => 9, 'weight' => 150]
];
Функция usort позволяет сортировать массив с помощью некоторой функции, очень простой. Однако в этом случае мы хотим отсортировать массив, используя его внутренний ключ 'value'
, что можно сделать следующим образом:
$comparisonFn = function($a, $b) {
return $a['value'] < $b['value'] ? -1 : ($a['value'] > $b['value'] ? 1 : 0);
};
usort($arr, $comparisonFn);
// ['key' => 'w', 'value' => 2] will be the first element,
// ['key' => 'w', 'value' => 3] will be the second, etc
Теперь, возможно, вам нужно снова отсортировать массив, но на этот раз с помощью 'key'
в качестве целевого ключа необходимо было бы переписать функцию:
usort($arr, function($a, $b) {
return $a['key'] < $b['key'] ? -1 : ($a['key'] > $b['key'] ? 1 : 0);
});
Как вы можете видеть, логика функции идентична предыдущей, однако мы не можем повторно использовать предыдущую из-за необходимости сортировки с другим ключом. Эту проблему можно решить с помощью класса, который инкапсулирует логику сравнения в методе __invoke
и определяет ключ, который будет использоваться в его конструкторе:
class Comparator {
protected $key;
public function __construct($key) {
$this->key = $key;
}
public function __invoke($a, $b) {
return $a[$this->key] < $b[$this->key] ?
-1 : ($a[$this->key] > $b[$this->key] ? 1 : 0);
}
}
Объект класса, реализующий __invoke
, является "вызываемым", он может быть используется в любом контексте, в котором может быть функция, поэтому теперь мы можем просто создавать экземпляры объектов Comparator
и передавать их в функцию usort
:
usort($arr, new Comparator('key')); // sort by 'key'
usort($arr, new Comparator('value')); // sort by 'value'
usort($arr, new Comparator('weight')); // sort by 'weight'
Следующие абзацы отражают мое субъективное мнение, поэтому, если вы хотите, вы можете прекратить читать ответ сейчас;): Хотя предыдущий пример показал очень интересное использование __invoke
, такие случаи редки, и я бы избегал его использования, так как это может быть сделано действительно запутанными способами, и, как правило, существуют более простые альтернативы реализации. Примером альтернативы в той же задаче сортировки может быть использование функции, которая возвращает функцию сравнения:
function getComparisonByKeyFn($key) {
return function($a, $b) use ($key) {
return $a[$key] < $b[$key] ? -1 : ($a[$key] > $b[$key] ? 1 : 0);
};
}
usort($arr, getComparisonByKeyFn('weight'));
usort($arr, getComparisonByKeyFn('key'));
usort($arr, getComparisonByKeyFn('value'));
Хотя этот пример требует немного большей близости с лямбдами|замыканиями|анонимными функциями, он гораздо более лаконичен, поскольку он не создает целую структуру классов только для хранения внешнего значения.
Я считаю, что эта функция существует в основном для поддержки новой функции закрытия 5.3. Замыкания представлены как экземпляры класса Closure
и могут быть вызваны напрямую, например, $foo = $someClosure();
. Практическое преимущество __invoke()
заключается в том, что становится возможным создать стандартный тип обратного вызова, а не использовать странные комбинации строк, объектов и массивов в зависимости от того, ссылаетесь ли вы на функцию, метод экземпляра или статический метод.
Это сочетание двух вещей. Вы уже правильно определили одного из них. Это действительно похоже на интерфейс Java IRunnable
, где каждый "работоспособный" объект реализует один и тот же метод. В Java метод называется run
; в PHP метод называется __invoke
, и вам не нужно заранее явно реализовывать какой-либо конкретный тип интерфейса.
Вторым аспектом является синтаксический сахар , поэтому вместо вызова $obj->__invoke()
вы можете пропустить имя метода, чтобы оно выглядело как хотя вы вызываете объект напрямую: $obj()
.
Ключевая часть для PHP, чтобы иметь замыкания, является первой. Языку нужен какой-то установленный метод для вызова объекта закрытия, чтобы заставить его делать свое дело. Синтаксический сахар - это просто способ сделать его менее уродливым, как в случае со всеми "специальными" функциями с префиксами двойного подчеркивания.
На самом деле вам не следует вызывать $obj();
в отличие от $obj->function();
, если вы знаете, что имеете дело с определенным типом объекта. Тем не менее, если вы не хотите, чтобы ваши коллеги-коллеги почесали в затылке.
Метод __invoke
оживает в разных ситуациях. Особенно, когда ожидается, что вы предоставите общий вызываемый объект в качестве аргумента.
Представьте, что у вас есть метод в классе (который вы должны использовать и не можете изменить), который принимает в качестве аргумента только вызываемый.
$obj->setProcessor(function ($arg) {
// do something costly with the arguments
});
Теперь представьте, что вы хотите кэшировать и повторно использовать результат длительной операции или получить доступ к ранее использованным аргументам этой функции. С регулярными закрытиями, которые могут быть массивными.
// say what? what is it for?
$argList = [];
$obj->setProcessor(function ($arg) use (&$argList) {
static $cache;
// check if there is a cached result...
// do something costly with the arguments
// remember used arguments
$argList[] = $arg;
// save result to a cache
return $cache[$arg] = $result;
});
Видите ли, если вам понадобится получить доступ к $argList
откуда-то еще или просто очистить кэш от заблокированных записей, у вас проблемы!
Вот и приходит __invoke
на помощь:
class CachableSpecificWorker
{
private $cache = [];
private $argList = [];
public function __invoke($arg)
{
// check if there is a cached result...
// remember used arguments
$this->argList[] = $arg;
// do something costly with the arguments
// save result to a cache
return $this->cache[$arg] = $result;
}
public function removeFromCache($arg)
{
// purge an outdated result from the cache
unset($this->cache[$arg]);
}
public function countArgs()
{
// do the counting
return $resultOfCounting;
}
}
С классом выше работа с кэшированными данными становится легкий ветерок.
$worker = new CachableSpecificWorker();
// from the POV of $obj our $worker looks like a regular closure
$obj->setProcessor($worker);
// hey ho! we have a new data for this argument
$worker->removeFromCache($argWithNewData);
// pass it on somewhere else for future use
$logger->gatherStatsLater($worker);
Это всего лишь простой пример, иллюстрирующий концепцию. Можно пойти еще дальше и создать универсальную оболочку и класс кэширования. И многое другое.
Заключение (на основе всего вышесказанного)
В целом я рассматриваю __invoke(){...} волшебный метод как отличную возможность для абстрагирования использования основных функций объекта класса или для интуитивно понятной настройки объекта (подготовка объекта перед использованием его методов).
Случай 1 - Например, предположим, что я использую какой-либо сторонний объект, который реализует __invoke магический метод, обеспечивающий таким образом легкий доступ к основным функциям экземпляра объекта. Чтобы использовать его основную функцию, мне нужно только знать, что параметры __метод вызова ожидает и что будет конечным результатом этой функции (закрытие). Таким образом, я могу использовать основную функциональность объекта класса с минимальными усилиями для расширения возможностей объекта (обратите внимание, что в этом примере нам не нужно знать или использовать какое-либо имя метода).
Абстрагирование от реального кода...
Вместо
$obj->someFunctionNameInitTheMainFunctionality($arg1, $arg2);
Теперь мы используем:
$obj($arg1, $arg2);
Теперь мы также можем передать объект другим функциям, которые ожидают, что его параметры будут вызываемый точно так же, как в обычной функции:
Вместо
someFunctionThatExpectOneCallableArgument($someData, [get_class($obj), 'someFunctionNameInitTheMainFunctionality']);
Теперь мы используем:
someFunctionThatExpectOneCallableArgument($someData, $obj);
__invoke также предоставляет удобный ярлык для использования, так почему бы его не использовать?