PHP 5.3 Волшебный метод __invoke

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

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

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

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

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

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

Ответы

Ответ 1

Этот ответ несколько устарел для написания в 2009 году. Пожалуйста, возьмите его с крошкой соли.

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

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

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

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

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

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

Ответ 2

Использование __invoke имеет смысл, когда вам нужен callable, который должен поддерживать некоторое внутреннее состояние. Допустим, вы хотите отсортировать следующий массив:

$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'));

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

Ответ 3

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

Ответ 4

Действительно, вы не должны вызывать $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);

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

Ответ 5

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

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

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

Ответ 6

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

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

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

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

вместо

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

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

$obj($arg1, $arg2);

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

вместо

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

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

someFunctionThatExpectOneCallableArgument($someData, $obj);

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