Как стать автором
Обновить

Комментарии 65

Спасибо за статью! Сам часто задаю вопросы о callable на собеседовании. К сожалению многие спотыкаются если вопрос начать именно зачем ввели такой странный магический метод __invoke.

В вашем последнем примере есть неточность, как вы сами написали callable не всегда может быть вызвана через скобки, поэтому безопасно всегда использовать call_user_func
Согласен, безопаснее именно через call_user_func(), но в своем собственном проекте позволительно и так. Если вы уверены, что никакой экзотики кроме нормальных лямбд в эту часть кода не просочится )))
Повторюсь вопросом из прошлого топика — когда вы реально в повседневном коде используете __invoke?
Ежедневно буквально, а что?

Типа такого постоянно пишу:
$cache = new Cache\Memcache;
return $cache(function() use ($conditions) {
  return SomeModel::findAll($conditions)
}, 600);

или
$uploader = new Http\Uploader();
$fileName = $uploader('image');
Возможно, дело вкуса, просто я вот за всю практику на PHP не припомню ни одного случая, когда не смог без него обойтись. Примеров, подобных вашим, тоже как-то не встречал. Вот что сделается в первом примере, что во втором? Надо лезть в код, а в случае нормально названных и вызванных методов у классов — уже код банально читается проще, можно что-то пропустить во время копаний, не отвлекаясь
В первом случае кэширование делается. Есть функция, возвращающая некие данные, она передается кэшеру. Тот вернет либо значение из кэша, либо, если оно «протухло», вызовет эту функцию, положит данные в кэш и вернет их. Это гораздо удобнее, чем бесконечные get/set, вся логика кэширования инкапсулирована в объект.

Во-втором случае объект класса Uploader загружает файл, пришедший с формы из поля 'image' и возвращает путь до удачно загруженного файла.

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

Помимо этого, в классы может понадобиться добавить еще пару методов.

Наконец, чем хуже $cache->get(function() {}), почему метод, скажем, проверки доступности кеша «менее неявный», чем функционал, вызываемый __invoke?

Не знаю. В фреймворках в качестве хорошей практики так пишут?
Наконец, чем хуже $cache->get(function() {})

Да ничем. Вон, в некоторых фреймворках бизнес-логика в роутере — и ничего, пользуются популярностью )))
Поэтому я совершенно спокойно отношусь и к ->get() и к ->__invoke() и даже к CacheFactory::getInstance()->getDriver('memcache')->get($key)

«Хорошие практики» в PHP еще с самом начале пути, так что ответ на ваш вопрос может дать только время. И PSR )))
То есть вполне невинный вызов get вы сравнили с бизнес-логикой в роутере. Ну ок.
Где же тут сравнение? Сравнение — это «хуже», «лучше», «равносильно» или, скажем, «поэтичнее» или «вкуснее».
Я же просто посетовал на общую неустроенность PHP-тусовки, в которой попытки группы PSR навести хоть какой-то порядок выглядят очень многообещающе, но пока что явно недостаточно.
И специально акцентировал внимание на том, что я спокойно отношусь к любому стилю кода, потому что отчетливо понимаю, что всё это временно и рано или поздно стили также стандартизируются, как и сам язык.
Явное лучше неявного? :)
Это всё равно что спросить «Готов к труду и обороне?»
Всегда готов!
Тогда странно, что вам больше нравится __invoke.
Вы опять приписываете мне какие-то свои домыслы. Не делайте так, пожалуйста. Я ни разу нигде не сказал, что __invoke() мне «больше нравится» (больше, чем что?)

Вы спросили — использую ли? Ответ — да, использую, вот примеры.
Вы спросили — чем же хуже просто ->get()? Я ответил — ничем.

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

Что???
Мне иногда кажется, что он старше большинства программистов пишущих на нём.
Вы не могли бы дать более подробный пример кода для такого кеширования? Хочется посмотреть, как люди делают.
Ссылку на гитхаб в идеале.
То, что выше, и есть фактически рабочий пример. Ну разве что ключ забыл написать.

Вот вам копи-паст из рабочего кода, например (с небольшими сокращениями):

        $getBlock = function () use ($template, $route) {
            $controller = $this->createController($route->module, $route->controller);
            $controller->action($route->action, $route->params);
            return $controller->view->render(
                $route->action . (!empty($template) ? '.' . $template : '') . '.block.html',
                $controller->getData()
            );
        };

        if (!empty($blockOptions['cache'])) {
            $cache = new Cache();
            $key = md5($canonicalPath . serialize($route->params) . $template);
            if (!empty($blockOptions['cache']['time'])) {
                return $cache($key, $getBlock, $blockOptions['cache']['time']);
            } else {
                return $cache($key, $getBlock);
            }
        } else {
            return $getBlock();
        }
В PHP использовал пару раз и пару раз видел в Doctrine2, но проверку типа на \Closure встречаю очень часто. В Scala приходилось использовать немного чаще. Но суть вопроса на понимание как устроены лямбды внутри.
С тех пор, как он появился, до недавнего времени использовал 1 раз, и то чтобы выпендриться. А вот недавно писал чатик на reactphp с его continuation-passing style — оказалось архиудобно таким образом оформлять actor-ы.
Zend Framework 2 View Helper
Для проверки, является ли переменная — функцией, которую можно вызвать, предпочитаю использовать instanceof

$x = function () { 
    print(1); 
};

if ($x instanceof \Closure) {
    call_user_func($x);
}
Мелкое уточнение $x instanceof \Closure не проверяет является ли переменная функцией. Она проверяет является ли переменная анонимной функцией.

<?php
$x = function() {
};
$y = 'trim';

var_dump($x instanceof Closure); // true
var_dump($y instanceof Closure); // false
var_dump(is_callable($x)); // true
var_dump(is_callable($y)); // true
Еще одно уточнение — анонимной функцией, определенной через «новый» синтаксис.
Впрочем, знать о существовании «старого» нынче совершенно не обязательно и даже, наверное, вредно.
Про «старый» синтаксис в документации

Warning
This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.
О чем я и говорю. Лучше и не знать о такой фиче.
Тоже читал статью и ждал такого абзаца «callable vs callback» — интересная тема, жаль упущена!
Непонятно противопоставление
callable vs callback


Некоторые библиотечные функции принимают в качестве аргумента функции. Например array_map или usort. Десятки их. Такую функцию-аргумент другой функции и принято называть callback.

Callback-ом может быть значение типа callable. Так что фактически это синонимы.
А можно пример кода, в котором используются данные возможности языка, для решения реальной задачи. Под реальной задачей понимаю работу с файлами, отправку сообщений, работа с конфигами.
Выше написал в ответ на аналогичный вопрос.
Спасибо! Второй пример наиболее показателен и понятен.
Из реальной жизни: есть класс описывающий выпадающий список HtmlSelect. Конструктор принимает массив опций. Но иногда не хочется прям так сразу готовить этот массив опций (делать запросы к базе, например). Вместо этого передадим в анонимную функцию, которая умеет строить и возвращать массив опций. А внутри HtmlSelect::render() мы проверим, запускать ли функцию или у нас на руках готовый массив значений. Эдакий DependencyInjection для бедных — вместо целого сервиса предоставляющего список опций мы передаём либо готовые опции (допустим, примитивные [1=>'Yes', 0=>'No']), либо функцию.
Вы сейчас рассказали о виджете CGridView из Yii ))
В качестве значения ячейки таблицы можно передать или имя поля модели, или функцию, которая построит значение. Причем о последнем мануалы умалчивают и разработчики очень редко знают.
На Yii не пишу, но это действительно удобно, зря мануалы умалчивают.
Pull request, исправьте ситуацию)
Смысла в первом Yii уже не вижу, второй не использую.
Возможно. У меня свой Yii3 есть )))
своя разработка?
Коллективная. Отчасти моя, отчасти «группы товарищей».
В публичном доступе? ссылочкой поделитесь?
Ждал этого вопроса. В личку поделюсь. К массовой публичности не готов.
Теряется смысл фреймворка, если писать свой. Намного больше преимуществ даёт использование одного из популярных + своей библиотеки компонентов к нему
Расскажите это Фабьену или Taylor Otwell
А мануалы не умалчивают. В них написано, что:

string a PHP expression that will be evaluated for every data cell using {@link evaluateExpression}

В свою очередь в помощи к evaluateExpression указано

Evaluates a PHP expression or callback under the context of this component

Да, но официально первый Yii работает на PHP <=5.2
Следовательно в нем самом нигде анонимные функции не применяются. Более того, в мануале рекомендуется жёсткий eval() в таких местах.

Хотя о чем мы, первый Yii уже нет смысла обсуждать в 2015 году…
Немного дополню. Если в классе объявлены магические __call или __callStatic, то проверка любого метода is_callable будет true.
<?php
class a {
	public function __call($foo, $arguments) {}
	public static function __callStatic($foo, $arguments) {}
}
$obj = new a();
var_dump(is_callable('a::bar')); // bool(true)
var_dump(is_callable([$obj, 'baz'])); // bool(true)
Совершенно верно. Это я как-то упустил, надо бы добавить в статью. Спасибо.
Вообще-то за вычетом абсолютно бесполезной формы "Baz::foo" всё остальное callable работает безо всяких call_user_func:

class Baz
{
    static function foo () { return 'foo'; }
    function bar () { return 'bar'; }
    function __invoke() { return 'baz'; }
}
function foo () { return 'foo'; }

$foo = new Baz;

$f = 'foo';
assert($f() == 'foo');

$f = [ Baz::class, 'foo' ];
assert($f() == 'foo');

$f = [ 'Baz', 'foo' ];
assert($f() == 'foo');

$f = [ $foo, 'bar' ];
assert($f() == 'bar');

$f = $foo;
assert($f() == 'baz');

// Fatal error: Call to undefined function Baz::foo()
//$f = 'Baz::foo';
//assert($f() == 'foo');

Хотя, может, уже и это починили, потому что у меня похапэ старый стоит.
Очень часто встречаю во всяких event disaptcher'ах регистрацию слушателей как [$this, 'methodName']
Так вот это работать и не будет в вашем примере. PHP 5.6.6

код
<?php

class a{
    function b () {
        echo 'b';
    }
}

$a = new a();
[$a, 'b']();

В 5.6 — так не будет, через временную переменную — будет. А в php7 вообще все хорошо — реализация AST исправила все подобные неконсистентности, работает и ваш пример, и любимая JS-разработчиками форма записи (function(){})();, и даже ($this->someCallableProperty)().
А (function(){return[new Exception,'rtrim'('ltrim ')(' getTrace')]()['file'];})()[0] будет работать? :) Особенно строки интересуют, потому что синтаксис получается забавный.
Да, в такой форме работает.
Ага, работает.

Вообще, во всех случаях, когда в PHP <7 приходилось заводить временную переменную, в PHP7 должно работать напрямую. Главное — правильно скобочки не забыть расставить в тех случаях, когда без скобочек такая запись означает что-то иное.
Это проблема исключительно на уровне синтаксического анализа, во время выполнения кода всё прекрасно работает. Просто комбинация скобок []() в костыль-дривен парсере не поддерживается. Вот так работать будет:

$f = [$a, 'b'];
$f();

Так как после «регистрации слушателей во всяких event dispatcher'ах» хранятся переменные, то проблемы не существует.

Можно попробовать в PHP 7. Вроде, там обещали человеческий парсер.

К слову, схожая ситуация при вызове колбэков из свойств: в языке не существует синтаксис ($this->a)(), тоже приходится писать в две строчки (в PHP 5.5, по крайней мере).

[ UPD: symbix А, ну сверху уже написали, что теперь оба случая нормально работают. ]

P. S. call_user_func и особенно call_user_func_array жутко тормозные, поэтому в новом коде их следует избегать по возможности.
В новом коде их следует избегать всегда, если не нужна совместимость с PHP <7 — хотя разница не такая и большая:

$ time ./php -r 'class X{function f($a, $b){}}; $x = new X(); for ($i=0; $i<1000000; ++$i) call_user_func_array([$x, 'f'], [1,2]);'

real 0m1.037s
user 0m1.024s
sys 0m0.012s

$ time ./php -r 'class X{function f($a, $b){}}; $x = new X(); for ($i=0; $i<1000000; ++$i) [$x, 'f'](...[1,2]);'

real 0m0.901s
user 0m0.885s
sys 0m0.012s
О, разворачивалка массива в аргументы тоже появилась…

Фраза «если не нужна совместимость с PHP <7» несколько забавно звучит, учитывая, что до релиза ещё дожить надо. :)
Ну почему, новые проекты, достаточно крупные, чтобы не быть готовыми к продакшену до выхода php7, уже можно на нем начинать разрабатывать вполне.
>> невозможно вызвать ее напрямую, с помощью $x(), мы получим ошибку вида «Fatal error: Call to undefined function Foo::bar()» И здесь нам на помощь приходит специальная функция call_user_func(), которая умеет обходить «острые углы» и вызывать значения псевдотипа callable, даже если это невозможно с помощью обычного синтаксиса.

Кому-нибудь известно, это баг или фича такая?
Особенность парсера языка. В следующей мажорной версии этих и подобных особенностей не будет.
Ясно, баг значит, так я и думал)
Нельзя сказать, что баг. Не смогли сделать универсально и объявили особенностью :)
Ахах, вот и правильно, я теперь тоже думаю, что это «особенность» такая, а никакой не баг)))
Дело в том, что на самом деле create_function() не создавала лямбда-функцию в современном понимании, фактически эта функция создавала именованную функцию с именем наподобие «lambda_1» и возвращала ее имя.
Не совсем так. Чтобы эти имена не пересекались с создаваемыми через обычный function, авторы языка первым символом поставили символ с кодом ноль:

var_dump(ord(create_function("",""))); // 0
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории