Ты решил написать свой фреймворк. Стоило оно того?
Как говорили классики, «я знал, что рано или поздно мы дойдем и до этого». Вот и я спустя много лет спокойной жизни с Symfony в рабочих и ReactPHP в пет-проектах вписался в создание своего фреймворка.
Но его история только начинается. А как было у тех, чье детище доросло до продакшн-уровня, но так и осталось нишевым решением? Я нашел человека, который знает ответ на этот вопрос — автора и ведущего разработчика аспектно-ориентированного фреймворка.
Привет! Перед вами частичная расшифровка моего подкаста «Между скобок» — интервью с интересными людьми из мира PHP. В этом выпуске поговорим с Александром Лисаченко, создателем фреймворка, основанного на идеях из Java, который часто гуглят гоферы.
Если вам удобней слушать — в аудиоверсии есть больше технических примеров.
Скорее всего, не все из нас сталкивались с такой вещью, как АОП — аспектно-ориентированное программирование в PHP. А Саша упоролся и сделал свое решение для втаскивания этой штуки в наши приложения.
Сергей Жук, Skyeng и DriftPHP: Давай начнём с азов — что такое АОП и какую проблему оно решает?
Александр Лисаченко, PHP Russia и Go! AOP: Идея не нова. АОП придумали в Xerox, чтобы решить проблему сквозной функциональности. И в мире Java подход активно используется, чтобы проверять такие функциональности, как авторизация, аутентификация, кэширование, логирование, управление feature toggle-ами, circuit breaker-ами.
Очень большой спектр задач можно решить с помощью аспектов. Давай на примерах:
- Мы хотим сделать транзакцию. Перед началом надо написать, что мы в неё входим. Если она завершилась успешно, написать, что всё хорошо. Иначе — написать, что ошибка.
- Другой пример — кэширование. Мы хотим проверить, есть ли у нас что-то в кэше. Если нет, то прогреваем кэш в методе, сохраняем что-то в кэш и потом возвращаем значение уже из него.
- Профилирование: зачастую непонятно, что происходит внутри каждого конкретного респонса, приложение на проде начало тупить. А включать Xdebug — опасно или не дадут.
- Еще одна хорошая и интересная возможность, которая реализуется с помощью аспектов — это circuit breaker, когда нам нужно проверять, что метод не падает, а если падает, то быстро его выключить и перестать ожидать ответ.
Если мы посмотрим на приложение, то увидим, что подобные участки кода попадаются везде и с этим не очень удобно работать. Это так называемая сквозная функциональность — она присутствует во всём приложении.
И нет никакого хорошего способа, чтобы её как-то вынести за скобки с помощью традиционного объектно-ориентированного программирования.
Да, конечно, мы можем попробовать взять декоратор —, но если мы хотим, чтобы наш класс имплементил тот же самый интерфейс, необходимо реализовать или сгенерировать каждый метод прямо в декораторе. То есть, иметь декораторы под все классы, которые мы хотим закэшировать.
Сергей Жук: Ок, понял. Но для этого уже были какие-то PECL-расширения, фреймворк даже был. Почему ты решил написать свой?
Александр Лисаченко: Да, ты правильно заметил, — уже существовал ряд решений. Но, как мне показалось, они обладали значительными недостатками.
Часть решений пыталась трансформировать исходный код, сразу встроить в конкретный класс конкретный advice — участок кода, который повторяется из одного метода в другой. С помощью xlt они генерируют один класс и кладут туда абсолютно всё. В общем, этот код лучше не открывать совсем)
Второй класс решений — это экстеншены. Например, PHP AOP — он, в принципе, довольно функционален, но делает всё в runtime. Когда мы добавляем один advice, вроде как всё хорошо, но добавляем второй — и скорость начинает пропорционально уменьшаться. Соответственно, на десяти advice-ах приложение начинает подтормаживать, а если мы добавляем ещё пару десятков, всё — гарантирован timeout.
Как-то так сложилось, что я увидел, как в фреймворке Lithium была реализована идея под названием «фильтры» — такой прообраз современных middleware-ов. Мы создаём какие-то заранее известные точки в программе, и к этим точкам можно применять фильтры — как до вызова, так и после. Эта идея показалась настолько интересной, что я решил написать приложение и вынести сквозную функциональность с помощью этих фильтров. Начал изучать, как это сделано в Java.
И понял, что в PHP это с наскоку не удастся реализовать. То был, наверное, самый интересный момент.
В целом, весь процесс написания фреймворка был постоянной борьбой с «нельзя» и «невозможно». Казалось бы, я не мог реализовать фундаментальные вещи, но тем интереснее было с ними разобраться.
Сергей Жук: Я просто не могу не спросить. Почему Go AOP? Первый релиз фреймворка состоялся в начале 2013-го, язык Go тогда уже был, а у тебя PHP. Это чтобы было как с JavaScript и Java, да?)
Александр Лисаченко: Первый коммит не равняется началу локальной разработки на моём компе) В тот момент Go ещё не был популярен. Я погуглил, увидел, что есть какая-то внутренняя разработка Google, она занимает какую-то свою нишу… И не стал заморачиваться.
Само название фреймворка я выбрал, исходя из внутреннего, так сказать, пожелания. От go — «иди», «вперёд», «делай».
Сергей Жук: А переименовать потом не было мыслей?
Александр Лисаченко: Формально, полное название фреймворка Go! AOPPHP. Но со временем я убрал PHP, потому что это ставится через Composer и, кажется, нет смысла делать масло масляным.
Пока я получаю дополнительный трафик: многие, кто пытается найти АОП для Go, попадают на мой фреймворк для PHP. Возможно, в какой-то будущей версии придется сделать акцент, что это не про Go. Пока issue на GitHub-е на этот счет мне никто не заводил. Никаких претензий от Google или сообщества тоже не было.
Сергей Жук: Ок, а как это реализовано с точки зрения клиентского кода? Вот у меня есть приложение, например, на Laravel-е, есть пара интеграций со сторонними сервисами, я хочу эти вызовы логировать.
Александр Лисаченко: Под Laravel уже есть специальный модуль. Он поставит тебе всё необходимое в систему, настроит. Тебе необходимо будет написать аспект — это будет сервис, ты его протегируешь и он автоматически подхватится ядром АОП. В самом аспекте тебе необходимо понять, в каких точках приложения, в каких классах и методах ты хочешь реализовать функционал кэширования. Ты можешь указать метод прямо сигнатурой (но вариант не особо гибкий, потому метод можно переименовать, а в аспекте он останется), либо пометить метод аннотацией. Второй вариант более гибкий: там, где надо закэшировать, просто пометил cacheable, а движок за тебя сделает кэширование и вызовет callback, который находится в коде аспекта.
В момент загрузки твоего класса Composer-ом фреймворк вмешивается и проверяет, есть ли к этому классу готовая версия в кэше со встроенным аспектом. Если есть, он сразу её отдаёт, в runtime-е никаких проверок дополнительных не производится. Если же кэша нет, то мы построим AST-дерево, а поверх этого дерева создадим класс рефлекшена, но без его загрузки в память. И в этот момент мы сможем изменить его так, как хотим, то есть вплести код аспекта внутрь этого класса.
Я не люблю лапшекод внутри аспектов и решил делить класс на две части.
Оригинальный класс остаётся практически неизменным — меняется только название. И появляется второй класс с оригинальным названием класса, который экстендит тот основной и может при необходимости переопределить несколько методов.
Почему, собственно, наследование. Есть очень много методов и классов, которые возвращают instance обратно. Например, известный всем chain-метод: мы вызываем цепочку методов у объекта, он возвращает $this. Если мы задекорируем, то первый вызов он отработает, но вот дальше аспект отвалится. Заодно с наследованием экономится память — потому что instance в памяти по-прежнему один.
Сергей Жук: Вся эта архитектура, движок, — ты с самого начала все продумал или?
Александр Лисаченко: Было много вещей, в которые пришлось погружаться. Например, я не был хорошо знаком с AST, поэтому изучил много дисциплин, связанных с описанием грамматик. И если посмотреть на мой фреймворк, то pointcut у меня реализует полноценную грамматику и имеет свой синтаксис — это, наверное, одно из больших достижений. Можно написать сколько угодно сложные выражения, например, «вызов любого публичного метода, не начинающегося с asset, имплементирует интерфейс такой-то внутри space-а такого-то».
Также я много копал внутрь PHP. Смотрел, где какие экстеншены, как они работают. Где-то сидел с профайлингом, что-то оптимизировал, тюнил: зато сейчас, если просто подключить АОП-фреймворк, приложению добавятся какие-то смешные 7–10 миллисекунд. На уровне классических 100 миллисекунд ответа даже незаметно, что вызывается подкапотно такой огромный фреймворк.
Сергей Жук: А есть специфика под разные PHP-фреймворки?
Александр Лисаченко: В принципе, АОП-фреймворк задумывался как общая библиотека, не требующая специфической обвязки. Основное условие — использовать Composer. Но вот с Symfony оно не очень дружит.
В Symfony слишком много чёрной магии, и когда она схлестывается с магией в моём фреймворке, побеждает сильнейший — Symfony.
В целом идея Symfony в том, что есть контейнер, надо им пользоваться и не изобретать отдельные фреймворки, чтобы получить функциональность АОП. Есть более традиционные способы: подключить бандл — скажем, JMS AOP или мой Symfony Go AOP bundle.
Сергей Жук: Давай поговорим про сообщество и конкурентов. Есть ли они у тебя?
Александр Лисаченко: Фреймворков, насколько мне известно, сейчас три. Есть Ray. Aop, но он не пригодится для продакшена, потому что не умеет эффективно работать с Composer-ом. Авторы Flow подают свой фреймворк под соусом, что у нас тут есть АОП. Есть еще что-то ещё рядом с китайскими фреймворками, поверх Swoole есть обвязки, —, но это всё на уровне экстеншенов, а экстеншены могут админы по соображениям безопасности не пропустить. У меня всё-таки классический фреймворк, и он остаётся живым на любых версиях.
Что касается сообщества. Тех, кто разбирается и понимает хорошо, наверное, всего человека четыре: я, один парень из Сербии и два человека с моей бывшей работы, которые участвовали во всём том, что я делал. Я им показывал, естественно, все свои наработки, результаты. Последние пару месяцев, как я менял работу, вкладывал в open source очень мало сил и энергии, но оно живёт и работает автономно.
Возможно, кто-то знает мою другую библиотеку Z-Engine — c ней мы можем иметь доступ ко всем структурам в памяти, классам, методам и всему что угодно в PHP.
Я планирую, как только освободится время, продолжить работу над Z-Engine и сделаю следующую версию фреймворка, базирующуюся уже на внутренних структурах самого языка. Оно будет работать практически как джавовский AspectJ. Цель — прийти к этому к восьмой версии PHP.
Сергей Жук: Это же почти полностью переписывать фреймворк?
Александр Лисаченко: Нет, у меня всё декомпозировано. Изменяется только процесс внедрения кода: буквально несколько классов, которые отвечают за то, чтобы внести правки в конкретный класс. И в данном случае этот класс будет делать не файлики в кэше с разными структурами, а будет в этот момент в runtime-е изменять OPcache и модифицировать структуры PHP в памяти.
Сергей Жук: А какое вообще отношение PHP-сообщества к этой теме? Я вот увлекаюсь асинхронным PHP, он мало кого оставляет равнодушным. У тебя как с этим дела?
Александр Лисаченко: Всегда найдутся те, кто скажет, что мы можем это сделать и без АОП, зачем нам это в приложениях. Я отвечаю: если вы не работаете в энтерпрайзе, аспекты вам не пригодятся. И если вы считаете, что у вас энтерпрайз, но у вас один сервис и 2–3 разработчика, — вам тоже такое не подойдёт) АОП хорошо работает в командах, где есть несколько десятков человек, каждый из них пишет в своём стиле, есть, как правило, несколько приложений, и, как правило, микросервисная архитектура.
Я знаю точно, что есть ряд крупных компаний, которые используют фреймворк у себя: французские, российские. Бывает, на почту прилетают какие-то сообщения с благодарностями: мол, думали, нам на месяц работы, но раскопали твой фреймворк и за пару дней сделали ту задачу. Ребята месяц работы своих разработчиков сэкономили, это здорово.
Сергей Жук: А ты ведешь какую-то просветительскую деятельность? Фреймворк у тебя достаточно взрослый, но я бегло поискал туториалы — негусто. Кажется, спроси у десяти человек, что такое АОП, девять скажут, что не знают.
Александр Лисаченко: Да, верно.
Сергей Жук: Хотя, может, хорошо, что не знают?
Александр Лисаченко: Это спорный вопрос) Но есть проблема, которая у меня была и есть — это документация. Я по своей природе не люблю писать документацию. Могу написать крутые решения, могу изобретать какие-то необычные вещи, библиотеки, но с документацией — это прямо боль.
Мне даже в один момент друг из Сербии предлагал: давай напишем. И даже начал её писать, но через некоторое время запал тоже закончился… Так и получилось, что документацией не богаты, скажем так.
Сергей Жук: Получается, естественный отбор? Самые стойкие, кто залезли, поковырялись, те и юзают…
Александр Лисаченко: Да, и это те люди, которые имеют достаточный уровень, чтобы не накосячить.
Сергей Жук: А ты получал какой-то фидбек от людей, которые использовали фреймворк так, как ты бы не догадался?
Александр Лисаченко: Да, были такие кейсы. Например, Михаил Боднарчук, который написал AspectMock. Он взял фреймворк и понял, что с его помощью можно решить проблему моканья финальных методов, классов и даже функций.
Еще одну историю подкинули ребята, которые рефакторили старое приложение, и у них не было тестов — в общем, всё по классике. С помощью моего фреймворка они записали, с чем вызывается каждый конкретный метод, и сделали глобальный аспект. Перед вызовом всех публичных методов во всех классах в этой папке записывалось, что вызывается и с чем возвращается: какие типы, какие значения. Затем они начали это приложение гонять под нагрузкой, чтобы получить все возможные состояния кода. У них получился автоматический набор значений, допустимых для каждого из методов, и возвращаемые значения. То есть они заснепшотили всё состояние, а потом натравили обратный аспект, который вызывал метод и проверял, изменилась ли логика.
По сути, им удалось реализовать автоматический рефакторинг кода без покрытия его тестами.
Это была настолько крутая идея, что я какое-то время думал, как сделать инструмент для legacy-приложений, чтобы можно было все классы зафризить, посмотреть, с чем и как они вызываются, а потом при рефакторинге проверять, что существующие связи не нарушены. Но реализовать ее в каком-то инструменте, который можно было бы заопенсорсить, пока не получилось.
p.s. Спасибо, что дочитали и дослушали! Другие выпуски подкаста можно найти здесь.