Pull to refresh

Comments 55

На PHP 5.3 обратил внимание на такую особенность: переменные, упомянутые в use(), если являлись «ссылками», то перестают ими быть.

Например

$contracts  = ...;
$services = ...;

foreach ($contracts as &$contract) {

    $contract['xxx'] = 'yyy'; // тут меняется $contracts
    $contractServices = array_filter($services, function($v) use ($contract){
         return $v['id'] == $contract['contractId'];
    });

    $contract['mmm'] = 'nnn'; // а здесь $contract уже живёт своей жизнью, $contracts не меняется

}


Проблему решает амперсенд — use (&$contract), но, имхо, такое поведение оказалось неочевидным.
Да. Поэтому и при рекурсивном определении замыкания в use передается ссылка с амперсандом, несмотря на то, что параметр и так объект и должен передаваться по ссылке. Скорее всего это потому что, use — это не передача параметров в функцию, а разрешение на доступ к переменным родительской области видимости после определения функции. Поэтому по-умолчанию происходит копирование значений, если явно не указана передача по ссылке.
Спасибо. Путает скорее название конструкции — «use», подсознательно предполагаешь, что оно будет использовать имеющиеся инстансы переменных вместо их клонирования :) Мысленно ставлю вам плюс.
Что-то я не понял смысла в амперсанде вот тут: foreach ($contracts as &$contract)
Вы просто не поняли суть замыканий. Перечисленное в use захватыется с теми значениями, которые оно было на момент вычисления выражения с замыканием.
Например для простоты, пусть у вас обычный нумерованный массив каких-то объектов (с индексами 0, 1, 2 и т.п.). На каждый объект вам нужно повесить одинаковый callback или event, но внутри этого callback-а вам нужно знать индекс обрабатываемого объекта в массиве, а взять снаружи скажем неоткуда (ну скажем этот callback вызывается из какой-то библиотеки, которая рассчитана на одиночные объекты и знать не знает про ваши массивы). Вот здесь вы используете как раз замыкания
for($i=0;$i<count($obj_array);++$i) 
    $obj_array[$i].onevent = function () use($i) { /*echo $i...*/}

Если бы не было замыкания, вы бы эту $i никак не вытащили.
Передать ее параметром в само замыкание не судьба?
Нет, если этот onevent вызывается какой-то сторонней библиотекой.
По-моему замыкания стоит использовать только в случае, когда очень(!) необходимо сохранить состояние и в обработчиках (не более 5 на событие). Остальное все от лукавого
Вот вам еще в копилку, из реального проекта, обертка для Doctrine DQL ExpressionBuilder:

Немного магии замыканий
    /**
     * Метод возвращает набор лямбда-функций
     * для удобного доступа к выражениями Doctrine DQL.
     *
     * @example
     *
     * <code>
     *     $query_builder = $this->createSelect();
     *
     *     // импортируем все лямбда-функции в текущую область видимости
     *     extract($this->getExpressions());
     *
     *     $query_builder->where
     *     (
     *          // построим WHERE с помощью лямбда-функций
     *          $andx
     *          (
     *              $eq($alias('user'), ':user'),
     *              $eq($alias('type'), ':type'),
     *              $in($alias('status'), array(':active', ':disabled'))
     *          )
     *     );
     * </code>
     *
     * @return      array
     */
    public function getExpressions()
    {
        if (!empty (static::$expressions))
        {
            return static::$expressions;
        }

        $expr_builder = $this->getExpressionBuilder();

        // :TRICKY:         Imenem          22.03.12
        //
        // Эта лямбда-функция создает прокси
        // к методу объекта ExpressionsBuilder.
        // После ее вызова с указанием имени метода
        // будет создана лямбда-функция,
        // которая вызывает одноименный метод ExpressionsBuilder,
        // передавая ему полученные параметры.
        $expr_proxy = function($method) use ($expr_builder)
        {
            return function() use ($method, $expr_builder)
            {
                return call_user_func_array(array($expr_builder, $method), func_get_args());
            };
        };

        $alias  = static::$alias;

        static::$expressions = array
        (
            'alias' => function($field) use ($alias)
            {
                return $alias . '.' . $field;
            },
            'andx'      => $expr_proxy('andx'),
            'orx'       => $expr_proxy('orx'),
            'eq'        => $expr_proxy('eq'),
            'neq'       => $expr_proxy('neq'),
            'gte'       => $expr_proxy('gte'),
            'lte'       => $expr_proxy('lte'),
            'in'        => $expr_proxy('in'),
            'notIn'     => $expr_proxy('notIn'),
            'between'   => $expr_proxy('between'),
            'not'       => $expr_proxy('not'),
            'exists'    => $expr_proxy('exists')
        );

        return static::$expressions;
    }

Пример DQL-запроса
    /**
     * Метод возвращает актуальные на текущий день
     * запланированные платежи по уровню баланса
     *
     * @param       DateTime        $date           Метка времени запуска
     * @param       int             $offset         Индекс первого элемента, который должен попасть в выборку
     * @param       int             $limit          Максимальное кол-во элементов в выборке
     *
     * @return      Acme\SomeBundle\Entity\ScheduleItem[]              Массив запланированных задач
     */
    public function findActualBalanceSchedules(DateTime $date = null, $offset = null, $limit = null)
    {
        extract($this->getExpressions());

        $pt_repo = $this->getRepo('PaymentTask');

        // получим лямбду, которая добавляет
        // алиас сущности PaymentTask к имени поля
        $pt_alias = $pt_repo->getExpressions()['alias'];

        // создадим подзапрос, который выберет
        // для каждой ScheduleItem все PaymentTask,
        // которые были созданы не позднее суток назад,
        // и не были до конца выполнены
        $created_payment_tasks = $pt_repo
            ->createSelect()
            ->select($pt_alias('id'))
            ->andWhere
            (
                $gte($pt_alias('started'), ':started'),
                $in($pt_alias('status'),   PaymentTask::getActiveStatusSet())
            );

        return $this
            ->createActualSelect(ScheduleItem::TYPE_BALANCE, $offset, $limit)
            ->andWhere
            (
                // выберем все запланированные задачи по балансу,
                // для которых не существует неоконченных платежных задач
                $not($exists($created_payment_tasks))
            )
            // выберем задачи, созданные не позднее суток назад
            ->setParameter('started', (new DateTime)->sub(new DateInterval('P1D')))
            ->getQuery()
            ->getResult();
    }
Как видим, замыкание как и лямбда-функция представляют собой объект класса Closure

Стоит уточнить, наверное, что лямбда-функция это частный случай замыкания.

А использование замыканий в PHP отчасти непопулярно, по-моему, из-за специфики PHP, работающего в CGI-режиме (логически). Грубо говоря нет никакой разницы писать function ($request_param, $config_param) или function ($request_param) use ($config_param), если $config_param вычисляется при каждом запросе так же как $request_param.
Примеры кода на PHP это классно, но по-моему большинство идей использования можно почерпнуть из en.wikipedia.org/wiki/Closure_(computer_science)
Я вот не вижу преимуществ использования замыканий. Почти все можно реализовать без их использования, но используя их, код становится сложнее и менее понятным, его сложнее поддерживать и развивать. В определенный момент поддержка такого кода может отнять времени больше чем было сэкономлено при написании кода с использованием замыканий.
У замыканий есть такое преимущество, как возможность легковесного хранения состояния. Рассмотрим пример в котором имеются:
1. Порционный итератор (получает абстрактный загрузчик данных, передает ему границы порции и получает в ответ данные)
2. Репозиторий сущностей, который не хранит состояние и его метод для получения данных, который требует множество параметров, кроме возможных границ порции.
3. Сервис, который использует доступ к репозиторию с помощью порционного итератора, который тоже не хранит состояние

Используя замыкание мы можем создать легковесный адаптер, не внося изменений в репозиторий, итератор или сервис:

    public function getActualScheduleItems(DateTime $date = null, DateTimeZone $time_zone = null)
    {
        $data_loader = function($offset, $limit) use ($date, $time_zone)
        {
            return $this->getRepo()->findActualPeriodicalSchedules($date, $time_zone, $offset, $limit);
        };

        return $this->createPortionIterator($data_loader);
    }
Понятное дело что удобнее, я этого не отрицал. Согласитесь, можно этот код написать и не используя замыкания. Я писал о том, что замыкания усложняют отладку и поддержку кода. А если неаккуратно ними пользоваться, то может еще и память течь.
Тогда подкрепите свои слова примером того, как можно переписать это без замыканий, не сохраняя состояние в сервисе или репозитории (это идеологически и архитектурно неправильно). Я вот утверждаю, что некоторые куски кода короче, элегантнее и понятнее (выберите любые два :) с использованием замыканий.
Просто взять и переписать, сохранив логику примера выше, красиво не выйдет. Весь дизайн кода будет отличаться, вот в примере предполагается сохранение состояние. Изменив дизайн, его можно сохранять в полях класса. Можно передавать постоянно аргументами, можно просто оформить в виде класса. Я не говорю что все сказанное применимо к этому примеру, замечу еще раз, если проектировать не предполагая использование замыканий, то задачу можно решить и без них. Просто иной подход будет.
Сервисы и репозитории в принципе не могут хранить состояние, поэтому в полях класса хранить его нельзя. Оформить в виде класса то, что можно оформить в виде замыкания — громоздко и вряд-ли более понятно. Куча мелких классов-адаптеров — куча мелких файлов в ФС. Передавать аргументами — требует идентичности интерфейсов, а соответственно изменения классов или наследования.
если проектировать не предполагая использование замыканий, то задачу можно решить и без них
Да, вот только зачем отвергать решения, которые позволяют решить задачу лучше и проще? Здесь как раз замыкания вполне соответствуют KISS. В любой точке, в которой возможна передача коллбэка, можно, а зачастую и нужно, использовать замыкание, чтобы не плодить методы/наследники-адаптеры.
Стоит, наверное, уточнить, что этот пример будет работать только с версии 5.4 из-за указателя $this внутри замыкания.
Любой, кто знает PHP сможет переписать пример так, чтобы он работал в 5.3:
    public function getActualScheduleItems(DateTime $date = null, DateTimeZone $time_zone = null)
    {
        $repo = $this->getRepo();

        $data_loader = function($offset, $limit) use ($repo, $date, $time_zone)
        {
            return $repo->findActualPeriodicalSchedules($date, $time_zone, $offset, $limit);
        };

        return $this->createPortionIterator($data_loader);
    }
Несомненно, я лишь хотел съэкономить немного времени тем, кто только разбирается с замыканиями и не знает про ньюанс с $this, тем более, что в статье про это ничего не сказано.
Очень удобно передавать callback во всякие array_filter. Бывает удобно передать функцию куда-то внутрь.
Замыкания прекрасны, пусть и не настолько удобны и лаконичны, как в ECMAScript. Главное использовать их с умом и только тем, где это действительно удобно и нужно, а не потому что «это круто, модно, современно». А то есть риск получить неизлечимую злокачественную опухоль.
$expr
->if(function(){ return $this->v == 4;})
->then(function(){$this->v = 42;})
->else(function(){})
    ->elseif(function(){})
->end()
->while(function(){$this->v >=42})
    ->do(function(){
        $this->v --;
})
->end()

ИМХО попахивает Монадами.
Крайний с конца пример как бы намекает нам на возможность развития функционального программирования в PHP.
Вообще-то я про другое имел ввиду ))
«замыкание как и лямбда-функция представляют собой объект класса Closure»
Автор, вы уж почитайте до конца что такое лямбда функция и что такое замыкание. Лямбда функция — это то что вы называете замыканием, и именно она есть обтект класса Closure, а замыкание это действие при котором этот обьект помнит что находилось вокруг него при создании (именно кусочек который прячется за оператором use).
Спасибо за уточнение. Вы правы.
Валидация — 1-й пример:

return frunction($v) use ($min, $max){


В слове frunction — ошибка
А минус-то за что? Странно…
Замыкания — элегантный способ делать Dependancy Injection. См. например, прекрасный мини-фреймворк Silex.
Предположим, у вас есть некий интерфейс (скажем той же callback функции), который вы менять не хотите или не можете.
Но вам в каком-то месте потребовалось сделать функцию с этим интерфейсом, которой нужно зачем-то лезть в базу данных.
Откуда эта функция возьмет объект соединения с БД?
До PHP 5.3. практически единственным способом было объявить глобальную переменную global $db, или ну или global $app; $db = $app->db;
Но это засоряет глобальную область видимости и не очень работает, если у вас несколько соединений с дб.
С замыканиями вы просто объявляете функцию с нужным интерфейсом и передаете ей линк на $db через use ($db) в момент объявления.
Простите, я вот понять не могу этих нововведений. PHP != Javascript. Он не событийный язык. Я не могу понять, зачем из танка самолет делать?
Лямбда функции не только в JS используются и не только для событий.
переменная = функция, use и т.д. есть и в С++11x
Замыкания редко вот прям чтоб необходимы, но во многих случаях просто удобны своим простым синтаксисом.
Ну как пример — таблица футбольного чемпионата, нужно сделать сортировку, кто какое место занял по заданной турнирной таблице (там трюки при равенствен очков), при этом очень хочется использовать стандартный алгоритм uasort (ну не писать же свой пузырёк в тысячный раз, правильно).
Попробуйте сделать это с замыканиями и без — поймете разницу.
Вы очень сильно ошибаетесь насчёт «PHP это не событийный язык». Костыли по поводу внедрения событий в PHP встречаются ещё со времён первой версии Pear'а, а сейчас это имеет массовый характер. Если во фреймворке нет поддержки событий (хуков), то он плох. Сейчас наступают времена асинхронности, а там без событий ну никак нельзя, поэтому готовьтесь. Читайте мануал по phpDaemon и привыкайте к событийности ))
Я, коллега, все прекрасно понимаю, и понимаю куда все движется. И готовлюсь — изучаю Node.js
И лично я считаю что в PHP это все и останется костылями, ибо чтобы это все _действительно_ работало, нужна другая идеология построения, как протокола, так и веб сервера, так и протокола http. И для человека, который хоть как то работал с javascript, все эти нововведения в PHP, ну ни как новыми не кажутся :-)
Ну а как же phpDaemon? Активно развивается. И хотя мне кажется, что nodejs местами попроще, в phpd есть свой скрытый смысл, преимущества над nodejs. Там кстати тоже уже есть и WS-клиенты и всякие XMPP-протоколы поддерживаются.
По мне, это костыль. Ну для того, чтобы это все действительно работало, нужно другой, абсолютно другой подход ко всей клиент-серверной структуре. Node.js — это и веб-сервер и язык программирование, все вместе. Именно это дает такую чудовищную мощность и гибкость. Linux + Apach + MySQL + PHP !== node.js. Я даже не знаю как выразиться точней, ибо я не могу языком PHP выразить то, что выражаю nodejs, и появление таких конструкций, как [] и {} в PHP 5.4, а также, замыканий, анонимных функций, глазами человека, который практикует javascript + nodejs выглядит… ну просто обескураживающей. Ну это просто мысли в слух, ни ради холивара. Мне очень нравится PHP, очень, и он действительно проще, чем javascript, и в первую очередь, на мой взгляд, благодаря синхронному выполнению и отсутствия событий, анонимных функций, замыканий и т.д.
Вы смотрели phpd? Там нет никаких Apache, он сам себе сервер. По сути это то же самое, что и nodejs, только на php. Без linux и mysql вы не обойдётесь, они сами по себе, отдельно.
К сожалению, того PHP, которого мы знали 5 лет назад, скоро уже не будет. Всё будет асинхронно и персистентно)
Сейчас наступают времена асинхронности

На удивление свежая фраза… для 2020-го.
На удивление свежий ответ на комментарий от 2012-го ))

Ну и раз этот тред поднят со дна, отмечу, что демонизация в PHP, как и событийность, так и не «взлетели» как следует.
В 7.4 появился preload, может, в 8.0 чего наконец сделают.
Моё мнение — javascript уже давно переплюнул PHP в этих делах. Вряд ли нужно подтягивать PHP к Javascript, ибо получится просто ещё один Javascript.
Наконецто можно нормально использовать array_map и прочее. Писать имена функций определенных рядом строкой, ну совсем неприятно было.
//Time: 0.4395
//Time: 0.0764
А мне это уже не кажется хорошей идеей
Не знаю что за цифры вы привели. У меня таких отличий в производительности не наблюдается.

test1.php
<?php
$array = range(0, 100000);
$result = array_map(function($element) { return $element+1; },$array);


test2.php
<?php
$array = range(0, 100000);
function test($element) { return $element+1;}
array_map('test',$array);

Разница не между способом вызова анонимной функции, а между обработкой каждого элемента данных с помощью анонимной функции и обычной итерацией массива.
Ясно, исходил из темы топика.
>>$name = Input::get('name', function() {return 'Fred';});
Вот зачем изобретать велосипед, если и короче и понятнее можно написать
$name=isset($_GET['name'])?$_GET['name'] :'Fred');
Пример может быть более сложным, например, значение по умолчанию нужно брать из базы.

$name = Input::get('name', function() use ($db){$config = $db->loadConfig(); return $config['default_name'];});


В Вашем же случае либо придётся написать дополнительное условие

$name=isset($_GET['name'])?$_GET['name'] :null); 
if (is_null($name))
  {
  $config = $db->loadConfig(); 
  $name = $config['default_name'];
  }


, либо при каждом запросе обращаться к базе.

$config = $db->loadConfig(); 
$default_name = $config['default_name'];
$name=isset($_GET['name'])?$_GET['name'] :$default_name); 

Only those users with full accounts are able to leave comments. Log in, please.