Готовимся к собеседованию по PHP: псевдотип «callable»

  • Tutorial
Не секрет, что на собеседованиях любят задавать каверзные вопросы. Не всегда адекватные, не всегда имеющие отношение к реальности, но факт остается фактом — задают. Конечно, вопрос вопросу рознь, и иногда вопрос, на первый взгляд кажущийся вам дурацким, на самом деле направлен на проверку того, насколько хорошо вы знаете язык, на котором пишете.
image
Вторая часть серии статей посвящена одному из самых сложных и объемных вопросов о современном PHP — что такое «callable»? Я постарался свести в один текст некий минимум знаний об этом вопросе.


Что такое callable?


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

А можно пример?


Да легко. Самый часто используемый в современном языке вариант callable — это анонимная функция.
$x = function ($a) {
  return $a * 2;
};

assert( 
  true === is_callable($x) 
);
assert( 
  4 == $x(2) 
);


Функция is_callable() как раз проверяет — принадлежит ли переданное ей значение псевдотипу callable. Разумеется, анонимная функция принадлежит этому псевдотипу и is_callable() вернёт true.

Анонимные функции можно присваивать переменным и затем вызывать с помощью этих переменных (что и продемонстрировано в примере). Разумеется, анонимная функция может быть передана в качестве аргумента в другую функцию или быть возвращена функцией с помощью оператора return, что вместе с семейством функций вроде array_map или array_reduce открывает для нас дорогу к функциональному программированию (узкую, надо сказать дорожку, все-таки PHP изначально не функциональный язык).

В PHP существует специальный системный класс Closure. Когда вы создаете новую анонимную функцию, по сути вы неявно создаете объект этого класса. Подробнее об этом можно прочитать в мануале: php.net/manual/ru/class.closure.php

Немного путаницы. Авторы версии 5.3, в которой впервые появился современный синтаксис анонимных функций, перепутали два понятия — собственно анонимная функция (лямбда-функция) и замыкание (замыкание переменной на контекст этой анонимной функции). Именно поэтому анонимные функции реализованы в языке с помощью системного класса Closure, а не, к примеру, Lambda, как стоило бы ожидать. Имейте этот факт в виду на собеседовании — многие интервьюеры сами путают понятия «лямбда-функция» и «замыкание». Впрочем, подробный рассказ о том, что такое «замыкание», выходит за рамки этой статьи.


Строка как callable и небольшая историческая справка


Строки в PHP вполне могут быть callable! В этом случае интерпретатор будет искать обычную, неанонимную функцию с именем, совпадающим с данной строкой и, в случае успеха, вызовет такую функцию.
function foo($bar) {
  return $bar * 2;
}

$x = 'foo';

assert(
  true === is_callable($x)
);
assert(
  4 == $x(2)
);


Таким образом можно вызывать как свои функции, так и библиотечные. Есть ряд ограничений — нельзя вызвать isset(), empty() и другие функции, которые фактически являются конструкциями языка.

Стоит заметить, что callable-строка может содержать в себе конструкцию вида 'ClassName::method' — это не возбраняется, такие строки тоже будут callable. Обратите внимание на особенность — скобки списка аргументов в таком случае не пишутся!
class Foo {
  public static function bar() {
    return 42;
  }
}

$x = 'Foo::bar';

assert( 
  true === is_callable($x) 
);
assert( 
  42 == call_user_func($x) 
);

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

Вас могут попытаться подловить вопросом — а как давно в PHP появились анонимные функции? Корректный ответ таков: «Современный синтаксис появился в версии 5.3, а ранее, со времен PHP 4, существовал способ создания анонимных функций с помощью функции create_function() Разумеется, сейчас этот способ имеет лишь исторический интерес. И должен у любого уважающего себя программиста вызывать такие же чувства, как оператор goto и функция eval() — желание никогда это не писать.»

Почему я пишу об этом казусе здесь? Дело в том, что на самом деле create_function() не создавала лямбда-функцию в современном понимании, фактически эта функция создавала именованную функцию с именем наподобие «lambda_1» и возвращала ее имя. А дальше работал уже знакомый нам механизм, когда string является callable


Callable массивы


Массивы в PHP тоже могут быть callable! Есть два основных случая, когда это работает. Проще всего показать их на примере:
class Foo {

  public static function bar() {
    return 42;
  }

  public function baz() {
    return 1.46;
  }

}

assert( 
  true === is_callable([Foo::class, 'bar']) 
);
assert( 
  42 == call_user_func([Foo::class, 'bar']) 
);

$x = new Foo;

assert( 
  true === is_callable([$x, 'baz']) 
);
assert( 
  1.46 == call_user_func([$x, 'baz']) 
);

Итак, массив, в котором нулевой элемент — это имя класса, а первый — имя статического метода, является callable. Ровно также, как и массив, состоящий из объекта и имени его динамического метода.

Стоит отметить интересную особенность (подметили читатели в комментариях). Если в классе Foo определен метод __call() или __callStatic(), то is_callable(Foo $foo, 'bar') или is_callable(Foo::class, 'bar') соответственно всегда будет true. Что, в общем-то, вполне логично.


Callable объекты


Да, в PHP возможно и такое. Объект вполне может быть «функцией», достаточно лишь определить в классе магический метод __invoke():
class Filter {

  protected $filter = FILTER_DEFAULT;

  public function setFilter($filter) {
    $this->filter = $filter;
  }

  public function __invoke($val) {
    return filter_var($val, $this->filter);
  }

}

$filter = new Filter();
$filter->setFilter(FILTER_SANITIZE_EMAIL);

assert( true === is_callable($filter) );
assert( 'foo@example.com' == $filter('f o o @ example . com') );

$filter->setFilter(FILTER_SANITIZE_URL);
assert( 'http://test.com' == $filter('http:// test . com') );

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

Тайп-хинтинг


В современных версиях PHP, начиная с 5.4, появилась возможность указывать псевдотип callable в качестве хинта типа аргумента функции.
function filter($value, callable $filter) {
  return $filter($value);
}

В случае, если переданное значение не будет callable, попытка вызова функции с таким аргументом приведет к фатальной ошибке «Catchable fatal error: Argument 2 passed to filter() must be callable»

Вместо заключения


Callable — одна из самых сложных и запутанных тем при изучении основ PHP. С одной стороны мы имеем современный строгий и чистый синтаксис лямбда-функций и замыканий, прекрасную возможность __invoke(), а с другой стороны — огромное и уже фактически ненужное историческое наследие, которое, видимо, никогда не будет выпилено из языка.

Знать это наследие важно. И не только потому, что в вашей работе с огромной вероятностью придется столкнуться со старым кодом, в котором могут использоваться разные трюки, вроде create_function(). В первую очередь знать такие вещи нужно для собственного самосовершенствования, чтобы понимать все плюсы и минусы разных подходов, конструкций и парадигм, и уметь выбрать нужные в нужный момент времени.

И, разумеется, чтобы не упасть в грязь лицом на собеседовании :)

Продолжение следует!

Литература

php.net/manual/ru/language.types.callable.php
php.net/manual/ru/function.call-user-func.php
php.net/manual/ru/functions.anonymous.php
php.net/manual/ru/class.closure.php
php.net/manual/ru/language.oop5.magic.php#object.invoke
habrahabr.ru/post/147620
habrahabr.ru/post/167181
fabien.potencier.org/on-php-5-3-lambda-functions-and-closures.html
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 65

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                        $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();
                        }
                
              0
              В PHP использовал пару раз и пару раз видел в Doctrine2, но проверку типа на \Closure встречаю очень часто. В Scala приходилось использовать немного чаще. Но суть вопроса на понимание как устроены лямбды внутри.
                0
                С тех пор, как он появился, до недавнего времени использовал 1 раз, и то чтобы выпендриться. А вот недавно писал чатик на reactphp с его continuation-passing style — оказалось архиудобно таким образом оформлять actor-ы.
                  0
                  Zend Framework 2 View Helper
                +3
                Для проверки, является ли переменная — функцией, которую можно вызвать, предпочитаю использовать instanceof

                $x = function () { 
                    print(1); 
                };
                
                if ($x instanceof \Closure) {
                    call_user_func($x);
                }
                
                  0
                  Мелкое уточнение $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
                  
                    0
                    Еще одно уточнение — анонимной функцией, определенной через «новый» синтаксис.
                    Впрочем, знать о существовании «старого» нынче совершенно не обязательно и даже, наверное, вредно.
                      0
                      Про «старый» синтаксис в документации

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


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

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

                                  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

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

                                    Хотя о чем мы, первый Yii уже нет смысла обсуждать в 2015 году…
                              +5
                              Немного дополню. Если в классе объявлены магические __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)
                              
                                0
                                Совершенно верно. Это я как-то упустил, надо бы добавить в статью. Спасибо.
                                0
                                Вообще-то за вычетом абсолютно бесполезной формы "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');

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

                                  код
                                  <?php
                                  
                                  class a{
                                      function b () {
                                          echo 'b';
                                      }
                                  }
                                  
                                  $a = new a();
                                  [$a, 'b']();
                                  

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

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

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

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

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

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

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

                                        P. S. call_user_func и особенно call_user_func_array жутко тормозные, поэтому в новом коде их следует избегать по возможности.
                                          0
                                          В новом коде их следует избегать всегда, если не нужна совместимость с 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
                                            0
                                            О, разворачивалка массива в аргументы тоже появилась…

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

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

                                          var_dump(ord(create_function("",""))); // 0
                                          

                                          Only users with full accounts can post comments. Log in, please.