PHP 5.3 Магический метод __invoke - PullRequest
20 голосов
/ 20 мая 2009

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

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

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

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

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

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

Ответы [ 6 ]

27 голосов
/ 20 мая 2009

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

Метод __invoke - это способ, которым PHP может поддерживать функции псевдо-первого класса.

Метод __invoke может использоваться для передачи класса, который может действовать как замыкание или продолжение , или просто как функция, которую вы можете передавать.

Много функционального программирования опирается на функции первого класса. Даже нормальное императивное программирование может извлечь из этого пользу.

Скажем, у вас была процедура сортировки, но вы хотели поддерживать разные функции сравнения. Ну, у вас могут быть разные классы сравнения, которые реализуют функцию __invoke и передают экземпляры в класс вашей функции сортировки, и ей даже не нужно знать имя функции.

Действительно, вы всегда могли бы сделать что-то вроде передачи класса и вызвать функцию, вызывающую метод, но теперь вы можете почти говорить о передаче «функции» вместо передачи класса, хотя это не так чисто, как в других языках. .

18 голосов
/ 08 февраля 2016

Использование __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);
    }
}

Объект Class, который реализует __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 | крышки | анонимные функции гораздо более кратки, поскольку не создают целую структуру класса только для хранения внешнего значения.

14 голосов
/ 20 мая 2009

Я считаю, что эта функциональность существует главным образом для поддержки новой функциональности закрытия 5.3. Замыкания представляются как экземпляры класса Closure и могут вызываться напрямую, например, $foo = $someClosure();. Практическое преимущество __invoke() заключается в том, что становится возможным создать стандартный тип обратного вызова вместо использования странных комбинаций строк, объектов и массивов в зависимости от того, ссылаетесь ли вы на функцию, метод экземпляра или статический метод.

3 голосов
/ 12 мая 2016

На самом деле вам не следует звонить $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);

Это простой пример, иллюстрирующий концепцию. Можно пойти еще дальше и создать общий класс-оболочку и класс кэширования. И многое другое.

3 голосов
/ 20 мая 2009

Это комбинация двух вещей. Вы правильно определили один из них уже. Это действительно так же, как интерфейс Java IRunnable, где каждый «работающий» объект реализует один и тот же метод. В Java метод называется run; в PHP метод называется __invoke, и вам не нужно явно реализовывать какой-либо конкретный тип интерфейса заранее.

Второй аспект - синтаксический сахар , поэтому вместо вызова $obj->__invoke() вы можете пропустить имя метода, так что создается впечатление, будто вы вызываете объект напрямую: $obj().

Ключевая часть PHP для замыканий - первая. Языку нужен какой-то установленный метод для вызова объекта замыкания, чтобы он делал свое дело. Синтаксический сахар - просто способ сделать его менее уродливым, как в случае со всеми «специальными» функциями с префиксами с двойным подчеркиванием.

1 голос
/ 18 мая 2017

Заключение (на основании всего вышеперечисленного)

Обычно я вижу магический метод __invoke () {...} как отличную возможность для абстрагирования использования основных функций объекта класса или для интуитивной настройки объекта (подготовки объекта перед использованием его методов).

Случай 1 - Например, допустим, что я использую какой-то сторонний объект, который реализует магический метод __invoke, предоставляя таким образом легкий доступ к основным функциям экземпляра объекта. Чтобы использовать его, мне нужно только знать, какие параметры ожидает метод __invoke и каков будет конечный результат этой функции (замыкание). Таким образом, я могу использовать основные функциональные возможности объекта класса без особых усилий для расширения возможностей объекта (обратите внимание, что в этом примере нам не нужно знать или использовать любое имя метода).

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

вместо

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

мы сейчас используем:

$obj($arg1, $arg2);

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

вместо

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

мы сейчас используем:

someFunctionThatExpectOneCallableArgument($someData, $obj);

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

...