Всем привет!

Закончили в этом месяце первый набор курса «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

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

Комментарии (6)


  1. ghost404
    29.06.2018 23:19
    +1

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


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


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


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


    1. zviryatko
      30.06.2018 22:21

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


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


  1. rjhdby
    30.06.2018 01:25

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

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


    1. SerafimArts
      30.06.2018 06:29

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


      Зато есть библиотеки, которые позволяют это делать не только с PHP кодом:
      1) github.com/hoaproject/Compiler
      2) И его форк: github.com/railt/parser


      1. Fesor
        02.07.2018 12:54

        Что ты думаешь на тему запилить библиотеку для комбинаторов парсеров?


        1. SerafimArts
          02.07.2018 18:12

          А можно чуть более приземлёнными словами, делая скидку на то, что я дилетант в этой области? =)