PHP 5.3 вызов магического метода


Эта тема расширяется на Когда я должен использовать _ _ construct (), __get (), __set () и _ _ call () в PHP? который говорит о __construct, __get и __set магические методы.

Начиная с PHP 5.3 существует новый магический метод под названием __invoke. Метод __invoke вызывается, когда скрипт пытается вызвать объект как функцию.

Теперь, когда я провел исследование этого метода, люди сравнивают его с методом Java .run() - см. интерфейс Runnable.

Подумав долго и я не могу придумать ни одной причины, по которой вы назвали бы $obj();, а не $obj->function();

Даже если бы вы перебирали массив объектов, вы все равно знали бы имя главной функции, которую вы хотите запустить.

Так является ли волшебный метод __invoke Еще одним примером ярлыка "только потому, что вы можете, не означает, что вы должны" в PHP, или есть случаи, когда это действительно было бы правильным?

6 17

6 ответов:

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 позволяет сортировать массив с помощью некоторой функции, очень простой. Однако в этом случае мы хотим отсортировать массив, используя его внутренний ключ arrays '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'));

Хотя этот пример требует немного большей близости с lambdas | closures | anonymous функциями, он гораздо более лаконичен, поскольку он не создает целую структуру класса только для хранения внешнего значения.

Я считаю, что эта функциональность существует главным образом для поддержки новой функциональности закрытия 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 magic, обеспечивающий таким образом легкий доступ к основной функциональности экземпляра объекта. Чтобы использовать его главную особенность мне нужно только знать, что параметры _ _ invoke метод ожидает и что будет конечным результатом этой функции (закрытия). Таким образом, я могу использовать основную функциональность объекта класса с небольшими усилиями, чтобы вложить возможности объекта (обратите внимание, что в этом примере нам не нужно знать или использовать какое-либо имя метода).

Абстрагирование от реального кода...

Вместо

$obj->someFunctionNameInitTheMainFunctionality($arg1, $arg2);

Теперь мы используем:

$obj($arg1, $arg2);

Теперь мы можем также передать объект другим функциям, которые ожидают, что его параметры будут вызывается так же, как и в обычной функции:

Вместо

someFunctionThatExpectOneCallableArgument($someData, [get_class($obj), 'someFunctionNameInitTheMainFunctionality']);

Теперь мы используем:

someFunctionThatExpectOneCallableArgument($someData, $obj);

__invoke также предоставляет хороший ярлык использования, так почему бы не использовать его ?