Декораторы в PHP

    image
    Решил поделиться своим видением и наработками по реализации python-style декораторов в PHP.
    В качестве завлекалочки небольшой пример использования на изображении справа. Выводит (после реализации логики самих декораторов):
    Log: calling b()
    int(42)
    

    Реализация выполнена в виде C расширения и не требует пересборки самого PHP. Но не заведется на хостингах, где нельзя загрузить свою so'шку.
    На данный момент код находится в стадии беты (весь нужный функционал написан, но баги и утечки памяти наверняка есть :) ). Так что as is. Ну а если есть желание помочь в развитии, то буду рад принять коммиты на github.


    Простой пример использования:
    <?php
    function double($func) {
        return function() use($func) {
            return 2*call_user_func_array($func, func_get_args());
        };
    }
    
    @double
    function a()
    {
        return 21;
    }
    var_dump(a());
    
    /* Вывод:
    int(42)
    */
    


    Декораторы всегда являются функциями, возвращающими функции. Внешняя функция принимает первым параметром подменяемую функцию. В отличие от python, декоратор с параметрами не описывается как функция, возвращающая функцию, которая возвращает функцию… Дополнительные параметры просто передаются после замещаемой функции:
    <?php
    function add($func, $v=0) {
        return function() use($func, $v) {
            return $v+call_user_func_array($func, func_get_args());
        };
    }
    
    @add(1)
    function a()
    {
        return 1;
    }
    var_dump(a());
    
    /* Вывод:
    int(2)
    */
    


    Декораторы можно комбинировать:
    <?php
    function dec($func, $p='[]')
    {
        return function() use($func, $p) {
            $s = call_user_func_array($func, func_get_args());
            return $p[0].$s.$p[1];
        };
    }
    
    @dec
    function a()
    {
        return 'I';
    }
    var_dump(a());
    
    @dec('{}')
    function b()
    {
        return 'am';
    }
    var_dump(b());
    
    @dec
    @dec('()')
    @dec('{}')
    function c()
    {
        return 'here';
    }
    var_dump(c());
    
    /* Вывод:
    string(3) "[I]"
    string(4) "{am}"
    string(10) "[({here})]"
    */
    


    При этом они выполняются в порядке обратном указанию:
    @A
    @B
    @C
    function X

    превращается в
    A(B(C(X(...))))


    Количество параметров и их типы являются произвольными, а ленивость вычислений вообще развязывает руки:
    <?php
    class Logger
    {
        const INFO  = 'INFO';
    
        public static function log($func, $text='', $level=self::INFO, $prefix='')
        {
            return function() use($func, $text, $level, $prefix) {
                printf("%s%s: %s\n", $prefix, $level, $text);
                return call_user_func_array($func, func_get_args());
            };
        }
    }
    
    @Logger::log('calling a()', Logger::INFO, date('Y-m-d H:i:s').': ')
    function a()
    {
        return 'Hello';
    }
    var_dump(a());
    
    /* Вывод:
    2013-05-24 14:22:23: INFO: calling a()
    string(5) "Hello"
    */
    

    В качестве имен декораторов должны выступать функции и статические методы, причем объявленные на момент вызова, а не при описании декоратора. Да и вообще можно поэкспериментировать:
    <?php
    class Arr
    {
        public static function map($func, $cb)
        {
            return function() use($func, $cb) {
                $v = call_user_func_array($func, func_get_args());
                return array_map($cb, $v);
            };
        }
    }
    
    class Foo
    {
        /* инвертируем знаки чисел в массиве */
        @Arr::map(function($i){return -$i;})
        /**
         * Комментарии между описанием декораторов и телом функций
         *   по большей части поддерживаются
         *
         * @return array
         */
        public function bar()
        {
            return range(1, 3);
        }
    }
    
    $foo = new Foo();
    print_r($foo->bar());
    
    /* Вывод:
    Array
    (
        [0] => -1
        [1] => -2
        [2] => -3
    )
    
    */
    

    Ну, я уверен, тут каждый сможет придумать что-то поинтересней в контексте своих задач.

    Технические вопросы


    На данный момент я проверил поддержку при выполнении кода с декораторами через:
    • cat file.php|php
    • php file.php
    • require/include
    • eval

    Возможно, что-то еще упущено.

    Из известных багов/фич (фичи можно обсудить; баги я скоро исправлю):
    • Если у декоратора указываются параметры, то открывающая '(' должна быть на той же строке, что и имя декоратора;
    • Вследствие модификации кода __FUNCTION__ и __METHOD__ теряют свою актуальность. Можно исправить подменой констант на строки с итоговыми значениями, но не уверен в правильности такого решения;
    • __LINE__ должен всегда совпадать, хотя случай многострочного описания параметров декораторов еще не проработан;
    • При ошибках синтаксиса описания декораторов вызывается исключение базового класса Exception с некорректными именем файла и номером строки;
    • Комментарии в коде на github на русском, т.к. моего уровня письменного английского не достаточно, чтобы не было стыдно за написанное. Надеюсь, временно, да и если кто пришлет коммит с хорошим переводом — будет классно!
    • Приличные IDE ругаются на непонятный синтаксис. Есть ли возможность обучить PHPStorm хотя бы не ругаться?


    P.S. Если опрос наберет достаточно вариантов «Нужны с большими подробностями», могу написать пост с подробным описанием Сишной реализации этого расширения.

    Only registered users can participate in poll. Log in, please.

    Нужны ли еще подобные статьи про другие PHP расширения?

    • 34.2%Нужны в подобном формате125
    • 39.4%Нужны с большими подробностями (C код, препарирование Zend'а)144
    • 26.3%Не нужны96

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 54

      +2
      За что ж вы так с @ то…
        +6
        @ — друг программиста теперь и в PHP
          –4
          В лучших традициях костыльно-ориентированного программирования PHP все ошибки, возникающие в декораторах будут подавляться по-умолчанию.
            0
            @ в объявлении декоратора не доходит до самого PHP. И никакого влияния на реакцию на ошибки они не оказывают.
            Это просто частичное совпадение синтаксиса.
              0
              А как подавить все ошибки, возникающие в декораторе при его исполнении? @@?
                +1
                Если без error_handler никак, то так же как и раньше — перед вызываемой функцией, которая может сгенерить ошибку.
                  0
                  error_reporting(0);
                    0
                    Ну да, этот как вариант error_handler'а.
                    Хотя можно извратиться так :)
                    <?php
                    function silence($func) {
                        return function() use($func) {
                            $current_level = error_reporting(0);
                            $res = call_user_func_array($func, func_get_args());
                            error_reporting($current_level);
                            return $res;
                        };
                    }
                    
                    function replacer($func, $name) {
                        return function() use($func, $name) {
                            return call_user_func_array($name, func_get_args());
                        };
                    }
                    
                    @silence
                    @replacer('fopen')
                    function xfopen(){}
                    
                    xfopen('/wrong/path', 'rb');
                    


                    Возможно, приду в последствие к чему-нибудь вида
                    @Decorate('fopen', 'silence')
                    или
                    Decorate('fopen', 'silence');

          +2
          За что ж вы так с @ то…

          А почему бы и нет? Его всё-равно никто не использует.
            –1
            Его нельзя «не использовать», потому что в php нет другого способа вручную обработать ошибки при вызове ряда стандартных функций. Например, если вы вызовете fopen без собаки, то получите warning для несуществующего файла (а вы, возможно, хотели эту ситуацию обработаьь вручную и показать пользователю внятное сообщение). На всякий случай для тех, кто сам не догадался: предварительный вызов file_exists, is_readable и т.д. проблему не решают, потому что между вызовами этих проверок и вызовом самой fopen файл может исчезнать (к примеру).
              +5
              А собственный error_handler?
                +8
                Есть два способа обработать эту ситуацию:

                1) Как написали выше, собственный error_handler
                2) display_errors в off, и контролировать возвращаемое значение от fopen. В случае ошибки придет false.

                И к слову сказать, я считаю, что это более правильные варианты, нежели подавлять ошибку с помощью @.
                  0
                  1) Если вы пишете библиотеку, то error_handler мягко говоря не самый удобный вариант, вам надо: выставить обработчик перед вызовом, выполнить операцию, вернуть обратно.
                  2) display errors не убережет вас от warning
                    +1
                    И правильно сделает, что не убережет. Но он не покажет сообщение пользователю, а запишет его в логи. Ситуация, когда файл пропадает после вызова file_exists и до вызова fopen явно не нормальна и ее надо отслеживать. Запись в логи в данном случае вполне уместна.
                      +2
                      Вызов file_exists и fopen это две дисковых операции, когда можно обойтись одним fopen и принципом let it fail.
                      Если я контролирую возвращаемое значение fopen мне абсолютно неинтересен этот warning, он будет только забивать логи.

                      В питоне мы можем сделать try fopen except IOError, в php такой возможности нет, поэтому приходится подавлять ошибку и проверять код возврата.
                        0
                        Ну если следовать принципу let it fail и нет возможности написать собственный error_handler, то такой подход допустим. Но я в своей практике ситуации где бы это было действительно оправдано встречал крайне редко.

                        Кстати, а если в Вашем примере без file_exists файл не пропал, а просто у скрипта нет прав на его чтение? В этом случае warning бы очень помог быстро обнаружить ошибку.
                          0
                          Подход в корне другой.
                          То что не удалось открыть файл, это еще не повод генерировать ошибку, я могу пытаться проверить файл на чтение как раз таким образом (вместо file_exists, т.к. file_exists не означает что файл можно будет открыть).
                          Или я могу захотеть проверить код возврата чтобы кинуть Exception, который будет отловлен на нужном уровне в моем коде.
                          Или у меня хостинг клиентских файлов, и то что файл не существует, или к нему нет доступа меня не волнует, может быть это администратор специально забрал права или удалил файл; в коде есть проверка результата fopen и показывается стандартная заглушка клиенту.
                          Короче говоря, отсутствие файла может быть вполне нормальной ситуацией, незачем об этом писать в логи.
                          +1
                          в php такой возможности нет?
                          www.php.net/manual/ru/class.errorexception.php
                            0
                            Это просто превращение всех ошибок в исключения. Не совсем то.
                            Нам-то нужно ловить именно аналог IOError (ошибки операции с файлом), а размытое понятие ErrorException ни о чем конкретном не говорит.
                              0
                              Функции стандартной библиотеки (разве что кроме «оберток» к методам ООП-библиотек типа mysqli_*) не бросают исключений, а сигнализируют об ошибках обычно возвратом FALSE (даже возвращающие при обычной работе 0, NULL, пустую строку и т. п. что часто приводит к конфузам из-за использования ==/!= вместо ===/!== в выражениях типа if ($result == FALSE) или if ($result != 0) ), а часть из них ещё бросают warning'и (может и другие ошибки бросают, но не припоминаю). @ как раз и предназначен (по крайней мере чаще всего для этого используется) для подавления warning'ов в таких случаях.
                            –1
                            Ситуация, когда файл пропадает после вызова file_exists и до вызова fopen явно не нормальна и ее надо отслеживать.

                            Для разделяемых между процессами (а то и хостами) файлов она вполне нормальна, пускай это и не лучший способ организовывать IPC, но частый, а иной раз вообще единственно доступный.
                            0
                            Так, по-моему, попроще и покорректнее будет.
                            $current_level = error_reporting(0);
                            $file = fopen('/etc/passwd', 'r+');
                            error_reporting($current_level);
                            

                            Единственное отличие в поведении вроде бы — не будет вызываться внешний error_handler
                          –2
                          А try except?
                            +3
                            не отработает, это не эксепшен
                              0
                              Не в курсе был, в python все отработает и перехватится…
                        +1
                        А по сути.
                        Выбрано по аналогии с python. Понятно, что выходит двоякое восприятие @. Но одной из целей написания статьи и было обсудить идею и синтаксис. Поменять @ на что-то другое, более подходящее, пока еще можно.
                          +1
                          Как вариант для синтаксиса использовать а-ля phpdoc аннотации.
                          Ну, например,
                          <?php
                          function double($func) {
                              return function() use($func) {
                                  return 2*call_user_func_array($func, func_get_args());
                              };
                          }
                          
                          /**
                           * @return integer
                           * @decorated_by double
                           **/
                          function a()
                          {
                              return 21;
                          }
                          var_dump(a());
                          
                          /* Вывод:
                          int(42)
                          */
                          

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

                            В общем, над этим еще думаю.
                              –1
                              Не имеет смысла использовать комментарии ни для чего, кроме комментирования; иначе — это убивает саму идею комментариев.
                                +1
                                А что делать если хранить и обрабатывать метаданные нужно, причем часто желательно в непосредственной близости от объекта, а синтаксис этого не позволяет.
                                  0
                                  Ответ может варьироваться в зависимости от того, что вы понимаете под метаданными.

                                  В нашем случае правильнее было бы говорить не о метаданных, а об инструкции/операторе, потому как на лицо его влияние на логику исполнения приложения. Ну, а добавление оператора означает расширение синтаксиса.
                                0
                                Из минусов — в декораторе может быть важная логика, например, авторизации и она не сработает.

                                Никогда никому не советую запихивать важную логику в декоратры… себе дороже…
                            0
                            код написан под впечатление статьи «Изменяем синтаксис РНР»
                            в целом не плохо.
                              0
                              А что за статья? Что-то ни на Хабре, ни гуглится по полному названию.
                              Да и если речь о модификации логики лексического парсера, то нет, у меня по другому.
                                +2
                                Мне ее нашли.
                                Нет, в моем случае не требуется пересобирать сам PHP: подцепляем расширение через «extension=decorators.so» и пользуемся.
                                  0
                                  ок…
                                  просто смотрю, синтаксис странный,
                                  да и твоя статья вышла три дня спустя…

                                  а вообще все супер
                              0
                              А неймспейсы поддерживает?
                                +2
                                Вопрос об этом?
                                1.php:
                                <?php
                                require(__DIR__.'/2.php');
                                
                                use \Foo\Decor;
                                
                                @Decor::neg
                                function one()
                                {
                                    return 1;
                                }
                                var_dump(one());
                                


                                2.php:
                                <?php
                                namespace Foo;
                                
                                class Decor
                                {
                                    public static function neg($func)
                                    {
                                        return function() use($func) {
                                            $v = call_user_func_array($func, func_get_args());
                                            return -$v;
                                        };
                                    }
                                }
                                


                                Выводит:
                                int(-1)

                                Вопрос был об этом?

                                Вариант с
                                @\Foo\Decor::neg
                                пока нет, но будет.
                                0
                                Я правильно понял, что в экстеншене:
                                1. Вручную написаный лексер пропускает через себя каждое выражение
                                2. Находя там собаку начинает собирать колбеки
                                3. Когда строки с собаками кончаются, то он собирает строку выражения вида call_user_func_array(call_user_func_array(%and so on%, func_get_args()), func_get_args()), и передает ее пхпшному парсеру?))

                                Это блин забавное решение:) Не подумайте, что я стебу, я даже плюсанул пост, но все же, способ очень извращенный)
                                  +1
                                  1) Да. Используется родной Zend'овский лексер lex_scan, поверх которого ДКА. Если в коде нет «собачек», то сразу выдается пришедшее на вход, без токенизации.

                                  3) Не совсем такая строка. Расширение экспортирует глобальную функцию decorators_preprocessor, которой можно подсунуть исходный код с декораторами, а на выходе получить чистый php код.
                                  Код вида:
                                  @a(A)
                                  @b
                                  @c(C)
                                  function x(X)
                                  {
                                      Y
                                  }
                                  

                                  превращается в:
                                  function x(X)
                                  { return call_user_func_array(a(b(c(function(X) {
                                      Y
                                  }, C)), A), func_get_args());}
                                  


                                  Я не придумал ничего лучше, с предпосылками:
                                  • просто php расширение, не затрагивающее сам PHP;
                                  • сохранение имени и формата вызова декорируемой функции/метода;
                                  • по максимуму сохранить неизменными магические константы.
                                  +3
                                  Просто оставлю это здесь github.com/aopalliance/php/wiki
                                    +3
                                    Вот здесь можно выхватить сложных багов:
                                    call_user_func_array($func, func_get_args());

                                    Не знаю как в свежих версиях PHP но раньше были проблемы в случае передачи параметров по ссылке.
                                    Насколько я помню func_get_args возвращала копию параметров и соответственно внутри функции по ссылке они уже не изменялись.
                                      +1
                                      Копируется и в 5.5.
                                      Не задумывался над этим вопросом (видимо из-за того, что не пользуюсь передачей по ссылке). Передача по ссылке в функцию, которая заворачивается в декоратор… Подумаю на эту тему.
                                      А вообще интересно мнение Хабра: как поступать в подобных случаях.
                                        +1
                                        Ну по сути проблема не большая, параметры по ссылке дейстивтельно используются не часто (но иногда бывает полезно при обработке больших массивов например).
                                        Тем не менее возможно стоило бы указать это как ограничение просто для того чтобы сэкономить кому то время и нервы.
                                          +2
                                          Пока расширение не имеет даже качественных тестов :)
                                          Когда будет версия с претензией на стабильность и оттестированность, которую сам попробую в production — тогда конечно будут задокументированы все особенности.

                                          А пока собираю дельные комментарии вроде вашего.
                                      –2
                                      Теперь модно вот это называть декораторами? Проблема в том, что это слегка, точнее совсем не декоратор. При этом если поле использования декораторов это именно рантайм, нацепить к примеру слой совместимости для объектов предыдущей версии, иль еще чего-нить. А это что? Если исходит из стиля PHP, это что-то похожее на трейты для функций. Да и рантаймом тут и не пахло.
                                      +3
                                      Я бы назвал все это аспектно-ориентированным программированием ) То, что вы делаете с помощью парсера очень походит на то, что делает моя либа, написанная на PHP. Я скоро планирую завершить работы и по перехвату стандартных функций, типа fopen, mysqli_open и др. Основное отличие-работать будет везде, на любом хостинге.
                                      Всех интересующихся приглашаю на DevConf )
                                        0
                                        АОП это то, что можно из декораторов получить, но на этом применение не заканчивается. Декораторы я планирую использовать в работе, так что самому интересно, к чему в итоге придет :)

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

                                        php -a

                                        Ну и надеюсь, что про mod_php и php-fpm (да со включенными apc? eaccelerator, xcache и т. п.) просто не сочли нужным упоминать в силу очевидности.

                                        А вообще интересно, хотя после знакомства с AOP декораторы python кажутся уже примитивными.
                                          0
                                          Ошибка — в первом же примере:

                                          /decor3.php — syntax error, unexpected 'function' (T_FUNCTION) in /home/..../php/decor3.php on line 9

                                          Версия php 5.4.15

                                          С остальными примерами то же самое.
                                            0
                                            Можете прислать в личку выдачу?
                                            echo decorators_preprocessor(file_get_contents('decor3.php'));
                                              –2
                                              Я прошу прощения, но после работы не сразу понял статью. Это был пример без указанного расширения, на чистом PHP. Не нашел, как удалить.
                                              А по поводу использования сразу могу сказать, что использовать не буду. По простой причине — мне необходимо отдавать код, который будет работать в том числе и на хостингах. Такова жизнь. А там и так хватает заморочек.
                                              Ставлю плюс. Виртуально. Кармы не хватает.

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