Pull to refresh

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

Reading time3 min
Views23K
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.25% Нужны в подобном формате125
39.45% Нужны с большими подробностями (C код, препарирование Zend'а)144
26.3% Не нужны96
365 users voted. 123 users abstained.
Tags:
Hubs:
Total votes 61: ↑45 and ↓16+29
Comments54

Articles