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

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

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

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

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

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

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


person Community    schedule 20.05.2009    source источник


Ответы (7)


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

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

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

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

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

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

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

person Kekoa    schedule 20.05.2009
comment
я не понимаю... какие именно функции первого класса, если не $a? $a = функция ($x,$y) {return ($x‹$y ? $x : $y;}; $min = array_reduce(массив(1,2,3), $a); - person stefs; 20.05.2009
comment
ооооо! хорошо, извините за то, что постоянно смешиваете анонимные/первоклассные/лямбда-функции. - person stefs; 20.05.2009
comment
первоклассные функции/замыкания в PHP 5.3: functionmultiBy($x) { return function ($y) use ($x) { return $x*$y;};}. $mulby3 = умножить на (3); $двадцать один = $mulby3(7); - __invoke тоже ›5.3, поэтому использование __invoke в качестве замены псевдопервоклассного не всегда имеет смысл. - person stefs; 20.05.2009
comment
Большое спасибо за ваш ответ, который прояснил некоторые вещи и предоставил мне много информации по этому вопросу. - person ; 21.05.2009
comment
Ваша первая ссылка скажет вам, что PHP действительно имеет первоклассные функции. en.wikipedia.org/wiki/First-class_function#Language_support - person Steve Clay; 06.07.2012
comment
@mrclay Очевидно, что в PHP дела обстоят лучше, чем когда я писал пост. К счастью, я не занимался PHP уже много лет. - person Kekoa; 06.07.2012
comment
-1; __invoke не был необходимым шагом для PHP, чтобы приспособить псевдопервоклассные функции; он был добавлен в той же версии, что и правильные анонимные (лямбда) функции. Таким образом, ваше объяснение намерений, стоящих за изменением, не имеет большого смысла. Обратите также внимание на то, что Python, в котором функции очень первоклассны, даже если ограниченный лямбда-синтаксис делает многие функции функционального программирования неуклюжими, поддерживает магический метод __call__, аналогичный методу __invoke в PHP. - person Mark Amery; 05.10.2014
comment
Этот ответ не немного устарел, он полностью устарел. PHP уже очень давно поддерживает первоклассные функции, и ответ основан на том факте, что первоклассные функции не поддерживаются. - person jpenna; 04.12.2019

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

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

person BrunoRB    schedule 08.02.2016
comment
не мог бы попросить лучшего объяснения. Спасибо. - person steady_daddy; 27.11.2016
comment
Преимущество __invoke перед Closure заключается в том, что вы получаете всю структуру, которую предоставляет class, включая принудительные интерфейсы, наследование и применение трейтов. Я мог видеть, что интерфейсы являются наиболее удобной вещью здесь. - person Josh from Qaribou; 31.07.2017
comment
Хорошо, что вы показали основанную на функции альтернативу вашему примеру. __invoke кажется аберрацией, чтобы продолжать использовать парадигму OO там, где это не нужно. - person jpenna; 04.12.2019

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

person jaz303    schedule 20.05.2009

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

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

person sanmai    schedule 12.05.2016

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

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

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

person Rob Kennedy    schedule 20.05.2009

Метод __invoke() вызывается, когда скрипт пытается вызвать объект как функцию.

Примечание. Эта функция доступна с PHP 5.3.0.

<?php
    class CallableClass
    {
        public function __invoke($x)
        {
            var_dump($x);
        }
    }
    $obj = new CallableClass;
    $obj(5);
    var_dump(is_callable($obj));
?>

Приведенный выше пример выведет:

интервал(5)

логический (правда)

Нажмите, чтобы увидеть ссылку.

person Ali Motameni    schedule 25.02.2020

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

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

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

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

вместо

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

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

$obj($arg1, $arg2);

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

вместо

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

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

someFunctionThatExpectOneCallableArgument($someData, $obj);

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

person DevWL    schedule 18.05.2017