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

в 11:22, , рубрики: decorators, extension, 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: %sn", $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 хотя бы не ругаться?

Автор: AterCattus

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js