Вызов волшебного метода PHP 5.3


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

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

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

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

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

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

Author: Community, 2009-05-20

6 answers

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

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

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

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

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

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

 24
Author: Kekoa, 2009-05-20 14:16:15

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

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

 16
Author: BrunoRB, 2016-02-08 18:58:27

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

 14
Author: jaz303, 2009-05-20 14:15:22

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

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

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

 3
Author: Rob Kennedy, 2009-05-20 14:24:35

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

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

 2
Author: sanmai, 2016-05-12 03:07:14

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

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

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

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

Вместо

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

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

$obj($arg1, $arg2);

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

Вместо

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

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

someFunctionThatExpectOneCallableArgument($someData, $obj);

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

 1
Author: DevWL, 2018-01-08 20:43:10