Аспектно-ориентированное программирование на PHP

http://radify.io/blog/aspect-oriented-programming/
  • Перевод
Всем привет!

Закончили в этом месяце первый набор курса «Backend разработчик на PHP» и трудоустраиваем их вовсю (ну насколько это возможно в пору отпусков). Курс пополнился ещё одним преподавателем — Евгением Волосатовым, которого многие, наверное, знают. Ну, а мы традиционно делимся интересными вещами.

Поехали.

В любом приложении есть части кода, “пересекающие” несколько частей архитектуры одновременно.

Эта проблема не так очевидна при работе с полнофункциональным фреймворком. Скорее всего ваша проблема окажется распространенной, и будет шанс, что фреймворк уже разрешил ее, пожертвовав разделением ответственности или предоставив абстракцию поверх фреймворка. Многие фреймворки используют событийно-ориентированную архитектуру для решения проблем сквозной функциональности. Но всегда наступает момент, когда фреймворк не способен предоставить нужный уровень контроля над конкретным фрагментом логики. Это особенно актуально при использовании микрофреймворков или разработке со специализированными библиотеками. Чем больше ваше приложение учитывает принципы разделения ответственности, тем больше будет роль сквозной функциональности в вашей архитектуре.



Аспектно-ориентированное программирование на PHP


Аспектно-ориентированное программирование (АОП) — парадигма программирования, сфокусированная на организации и модульности сквозной функциональности. Кейсы применения — ACL, логирование, обработка ошибок, кэширование.

Встроенный (внутренние) предположения PHP (когда вы определяете функцию/константу/класс, она остается определенной навсегда) делают парадигму АОП сложной для имплементации.

Li3 был первым, решившим проблему сквозной функциональности с помощью механизма фильтрации, позволяющего фильтровать логику метода через замыкания. Чтобы сделать метод фильтруемым, имплементация Li3 требует мануального добавления шаблонного кода. С такими ограничениями, техники АОП ограничиваются методами фильтрации.

Другие хорошо известные реализации АОП на PHP:

  • AOP (расширение PECL)
  • Go!

Расширение PECL AOP — интересный, но в то же время рискованный подход, так как поддержка PECL расширений не распространена. Другой вариант — библиотеки Go!, представляющие собой реализацию АОП, исправляющей PHP код на лету, что делает возможным использование методов АОП.

Есть и другие реализации, но большинство из них строятся на базе прокси (насколько мне известно), а у такого подхода множество ограничений.

Новый парень на районе


Автоматическая генерация кода давно есть в PHP и используется во многих библиотеках, например ProxyManager. А благодаря принятию Composer, Go! показывает, что правка кода на лету возможна.

Если генерацию кода еще можно посчитать простой, то исправление кода несколько сложнее. Во-первых, в PHP нет встроенного парсера кода, а во-вторых, очень мало библиотек, решающих проблемы парсинга PHP кода. Самая известная — библиотека PHP-Parser. PHP-Parser — отличный инструмент, но даже он игнорирует форматирование пробелов в сгенерированных абстрактных синтаксических деревьях. Что затрудняет исправление кода. Действительно, код, который нужно исправить, является настоящим исполняемым кодом. Поэтому, если хотите, чтобы обратная трассировка была точна при ошибках, нужно учитывать номера строк в исправленном файле.

Для этой задачи мы используем патчер JIT кода Kahlan. Kahlan — новый тестовый фреймворк Unit & BDD, благодаря JIT техникам правок, позволяющий stub’ить и monkey patch’ить код прямо в Ruby или JavaScript. Под капотом обнаружим, что данная библиотека основана на рудиментарном парсере PHP. Но, тем не менее, он достаточно быстрый и стабильный, чтобы нам подойти.

Библиотека фильтров доступна на github.com/crysalead/filter и может быть использована следующим образом.

Во-первых, патчер JIT кода должен быть инициализирован как можно быстрее (например, сразу после включения композера autoloade):

include __DIR__ . '/../vendor/autoload.php';

use Lead\Filter\Filters;

Filters::patch(true);

Заметим, что правка кода возможна только для классов, загруженных autoload’ером Composer. Если класс добавлен при помощи require или include выражения, он уже загружен перед вызовом Filters::patch(true), и поэтому не будет исправлен.

По умолчанию, весь исправленный код будет храниться по адресу /tmp/jit, но вы всегда можно изменить его на свой:

Filters::patch(true, ['cachePath' => 'my/cache/path/jit']);

Кэшированные файлы будут восстановливаться автоматически каждый раз при изменении PHP файла.

Внимание! Filters::patch(true) — самый простой способ настройки патчера, имейте в виду, что весь ваш код будет исправлен. Чтобы завернуть все методы вашей базы кода в фильтр-замыкание, может потребоваться много времени.

К счастью, если сквозной функционал играет решающую роль в хорошо продуманном коде, потребуется только пара методов в проекте. Поэтому предпочтительным подходом будет исправление только тех методов, что будут отфильтрованы:

Filters::patch([
 'A\ClassName',
 'An\Example\ClassName::foo',
 'A\Second\Example\ClassName' => ['foo', 'bar'],
], [
    'cachePath' => 'my/cache/path/jit',
]);

Таким образом, вы можете выбрать исправление всех методов определенного класса, только одного метода или нескольких из них.

API Фильтр


Теперь, когда JIT патчер включен, создадим фильтр логирования:

use Chaos\Filter\Filters;
use Chaos\Database\Database;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('database');
$logger->pushHandler(new StreamHandler('my/log/path/db.log', Logger::WARNING));

Filters::apply(Database::class, 'query', function($next, $sql, $data, $options) use ($logger) {
    $logger->info('Ran SQL query: ' . $sql);
    return $next($sql, $data, $options);
});

В приведенном выше примере создается фильтр регистрации SQL запросов для библиотеки базы данных Chaos. Больше информации о API фильтре можно узнать на github.com/crysalead/filter.

Заключение


АОП, по моему мнению, настоящий ответ для сквозного функционала. Все прочие абстракции одинаково излишни и в большинстве случае ограничены определенными рамками. Я не теряю надежду, что однажды PHP предоставит встроенный API для аспектно-ориентированного программирования. Но сейчас, я думаю, патчинг JIT кода — лучший вариант, где преимущества значительно перевешивают оверхэд CPU, которым можно практически пренебречь, когда патчинг JIT кода не применяется глобально.

THE END

Как всегда ждём, если что, вопросы, пожелание и приглашаем на открытый урок, где вы сможете послушать интересную лекцию и поближе познакомиться с новым преподавателем.

Отус

308,00

Профессиональные онлайн-курсы для разработчиков

Поделиться публикацией
Комментарии 14
    +2

    Делали подобные штуки лет 5 назад, в самописе, и кажется использовали Go!, только аспекты накладывали через аннотации.


    Пример в статье не очень удобен, потому, что он моментально сломается если метод переименуется или изменится список аргументов. А отслеживать такие вещи сложно ибо разработчик меняющий метод может быть не в курсе, что на него кто-то налажал аспект. И IDE такую связь не увидит. Особенно весело, если проблема вылезет на этапе слияния с другими фичами.


    Аннотации дают более четкую связь метода и аспекта и не такую жёсткую связь как явное использование в методе.


    Сейчас, в Symfony, подобные задачи можно решить через Doctrine Annotation.

      +1

      Полностью вас поддерживаю. Отслеживать это будет невозможно. Либо использовать какие-то дополнительные техники чтобы оправдать АОП.


      Как по мне нужно сначала определить на сколько огромное приложение, если речь идет о 2-6 "аспектах", например таких популярных как ACL и логгер, то хватит и того чтобы их оставить сквозными, ничего плохого не случится, куча cms тому пример. Если уже 6-25 аспектов, то основная проблема не потерять ни один из них при рефакторинге, и тут уже придется что-то использовать как например скрипты для проверки совместимости аспектов и/или интеграционные тесты, но и в этом случае хватит обычных декораторов, например в симфони контейнере можно объявить сервис декоратор, он отлично справится с такой задачей. А дальше веселее, если у вас 25-100+ аспектов, у рядового разработчика точно не будет понимания всей архитектуры и он фактически будет работать только с отдельными модулями, а тут чтобы оставить прозрачность и совместимость нужно использовать публичные интерфейсы между модулями, т.е. апи, и на роль этого апи отлично подходят ивенты, в вашем примере именно код модуля базы данных должен создавать ивенты (а с новыми версиями отмечать как устаревшие и добавлять новые) и все аспекты должны полагаться на ивенты и только в таком случае вы сократите риски при дальнейшей разработке.

      0
      Во-первых, в PHP нет встроенного парсера кода

      Токенизацию можно даже на уровне скрипта посмотреть, ASP и опкод доступны через расширения. Или что-то другое имелось в виду?
        0
        Если генерацию кода еще можно посчитать простой, то исправление кода несколько сложнее. Во-первых, в PHP нет встроенного парсера кода, а во-вторых, очень мало библиотек, решающих проблемы парсинга PHP кода


        Зато есть библиотеки, которые позволяют это делать не только с PHP кодом:
        1) github.com/hoaproject/Compiler
        2) И его форк: github.com/railt/parser
          0
          Что ты думаешь на тему запилить библиотеку для комбинаторов парсеров?
            0
            А можно чуть более приземлёнными словами, делая скидку на то, что я дилетант в этой области? =)
              0
              ну вот пример на JS: github.com/jneen/parsimmon

              под PHP есть вот такая статейка: inviqa.com/blog/functional-programming-php-developers-guide-parser-combinators-phunkie

              там же обзор библиотеки [phunkie](https://github.com/phunkie/phunkie), которая отдельно интересна.
                0
                ну вот пример на JS: github.com/jneen/parsimmon

                Ну так это ж лексер обычный

                под PHP есть вот такая статейка: inviqa.com/blog/functional-programming-php-developers-guide-parser-combinators-phunkie


                Ну так ты кидал эту статью уже. Я просто чуток не понимаю что в конечном итоге хочется получить? Декларацию правил парсера а-ля Yay? Это же просто функциональный интерфейс для декларации грамматик и всё. Не?
                  0
                  Ну так это ж лексер обычный

                  нет, это именно парсер. Там на выходе AST. просто "лексемы" выделяются как листья дерева парсеров.


                  Декларацию правил парсера а-ля Yay?

                  именно так, yay пример комбинатора парсеров. И оно не "просто" а устраняет много нюансов описания контекстно-зависимых грамматик.

                    0
                    нет, это именно парсер. Там на выходе AST. просто «лексемы» выделяются как листья дерева парсеров.


                    А, да, вижу: github.com/jneen/parsimmon/blob/master/API.md#parsernodename
                    Интересная штука.

                    именно так, yay пример комбинатора парсеров. И оно не «просто» а устраняет много нюансов описания контекстно-зависимых грамматик.


                    А чем тебя BNF/EBNF не устраивает в этом случае, который позволяет генерить правила сабжевого парсера, что по ссылке в первом моём комменте?

                    А если в целом, то да, мне идея API в стиле yay нравится, т.к. с тестами просто упороться можно. Приходится руками составлять таблицу переходов, а потом не понятно, то ли в грамматике накосячил в тестах, то ли реально в самом LL парсере где бага.

                    P.S. Оффтоп: Можно, кстати, попробовать действительно набросать грамматику для PHP. Авось и кому понравится, в качестве альтерантивы nikic парсера. Но она всё же LALR, через LL будет сложно нормально приоритеты операторов распарсить =\
                      0
                      А чем тебя BNF/EBNF не устраивает в этом случае

                      1. медленно (комбинаоры парсеров будут не быстрее, в силу того что лексинг по сути будет работать аналогично — тупо preg_match по оффсету, но в целом функции можно подменять. Вот будет FFI в PHP и можно будет re2c юзать для генерации таблиц сравнения символов). Я пока ленюсь сделать бенчмарк и сравнить json парсер на написанном наколенке комбинаторе парсеров и hoa (хочется еще сравнить какой-нибудь PHP-Yacc или pacc.
                      2. неудобно расширять грамматику (по сравнению с простой композицией функций). Как по мне для проектов типа Doctrine ORM это будет намного более интересной вещью нежели hoa.
                      3. стильно модно молодежно.

                      набросать грамматику для PHP

                      вот тут помогут фичи типа нэймспейсов для лексем, в силу того что у php таки контекстно зависимый лексер.


                      через LL будет сложно нормально приоритеты операторов распарсить

                      да там как бы не только в этом вопрос.

                        0
                        медленно (комбинаоры парсеров будут не быстрее, в силу того что лексинг по сути будет работать аналогично — тупо preg_match по оффсету, но в целом функции можно подменять. Вот будет FFI в PHP и можно будет re2c юзать для генерации таблиц сравнения символов).

                        Тут проблем ну вообще никаких нет. 600к токенов в секунду, вполне достаточно для моментальной токенизации даже мегабайтных файлов:image

                        И прошу заметить — это обычный домашний ПК под виндой, а не сервер. Да и места под дальнейший тюнинг тоже километр, например pthreads заюзать.

                        Я пока ленюсь сделать бенчмарк и сравнить json парсер на написанном наколенке комбинаторе парсеров и hoa (хочется еще сравнить какой-нибудь PHP-Yacc или pacc.

                        Да какая разница в итоге-то? Это всего лишь разные API гейтвей к парсеру. А производительность зависит от реализации рантайма к которому применили сабжевую грамматику. И LL, очевидно, будет быстрее и проще LALR реализации того же yacc/bison.

                        неудобно расширять грамматику (по сравнению с простой композицией функций).


                        А вот тут ты не прав. Всё упирается в реализацию. В моём случае парсер стейтлесс и поддерживает переопределение именованных групп. А значит поверх базовой можно вполне класть и кастомную. Проблемы только в генерации конечного парсера через компилятор компиляторов. Ну т.е. при каждом изменении грамматики её надо будет заново считывать и генерировать конечный класс.

                        стильно модно молодежно.


                        А это уже грязные инсинуации, молодой человек. Хотя аргумент — железный :D

                        да там как бы не только в этом вопрос.

                        А в чём тогда?
                          0
                          например pthreads заюзать.

                          я думаю это тупиковая ветвь. Нет смысла. А раскидать парсинг нескольких файлов проще через простой proc open.


                          Ну и опять же, я говорю о контекстно зависимых лексерах и возможности УДОБНО писать парсеры. Есть интересные проекты на том же ocaml которые вроде как довольно быстрые, да и имея декларативное описание парсера из него можно сгенерить что-то более эффективное.


                          А вот тут ты не прав.

                          что может быть проще декорации/композиции функций?)

                            0
                            я думаю это тупиковая ветвь. Нет смысла. А раскидать парсинг нескольких файлов проще через простой proc open.


                            Нужен ресёрч. Прям так сказать сложно. Но в любом случае его можно без особого напряга заюзать для мультистейт лексеров (ну типа неймспейсы из Hoa).

                            что может быть проще декорации/композиции функций?)

                            Грамматика с возможностью ссылаться декларируемые правила до и после.

                            digit ::= "1" | "2" | "3" | "4" | "5" ...etc ;
                            int ::= digit ;
                            float ::= [digit] "." digit ;
                            sum :: = (int | float) "+" (int | float | sum) ; <== ага, я могу ссылаться сам на себя! Читер! =)


                            Во время композиции теряется возможность получить ссылку на набор правил задекларированных до или после.

                            НО. Это всё опять же зависит от проектировки API. Так что вполне возможно, как это, например, сделано в yay, но получится это довольно избыточным с текущим синтаксисом пыха.

                            Ну и опять же, я говорю о контекстно зависимых лексерах и возможности УДОБНО писать парсеры. Есть интересные проекты на том же ocaml которые вроде как довольно быстрые, да и имея декларативное описание парсера из него можно сгенерить что-то более эффективное.


                            А я повторюсь, что (E)BNF like грамматика, изначально спроектированная для этого, не уступает по удобству композиции функций. Пока аргументов с твоей стороны в эту сторону не было. Так что предлагаю начать с этого. ;)

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

      Самое читаемое