Школа магии PHP. Часть 2
https://habr.com/ru/company/oleg-bunin/blog/478618/Часть 1 тут:
https://telegra.ph/SHkola-magii-PHP-12-04
Трюк #6. Обработка потоков
Рассмотрим конструкцию.
include 'php://filter/read=string.toupper/resource=magic.php';
Тут нечто волшебное: PHP-фильтр, read
, в конце подключается какой-то файлик magic.php. Этот файлик выглядит довольно просто.
<?php echo 'Hello, world!'
Заметьте, что регистр разный. Однако, если «заинклюдим» файлик через нашу конструкцию, то получим вот это:
HELLO, WORLD!
Что произошло в этот момент? Использование конструкции PHP-фильтра в include
позволяет подключить любой фильтр, в том числе и ваш, для анализа исходного кода. Вы управление всем, что находится в этом исходном коде. Можно убрать final
из классов и методов, сделать свойства публичными — всё, что угодно можно провернуть через эту штуку.
На этом базируется часть моего аспектного фреймворка. Когда подключается ваш класс, он его анализирует и трансформирует в то, что будет выполняться.
Из коробки в PHP есть уже целая пачка готовых фильтров.
var_dump(stream_get_filters()); array (size=10) 0 => string 'zlib.*' (length=6) 1 => string 'bzip2.*' (length=7) 2 => string 'convert.iconv.*' (length=15) 3 => string ' string.rotl3' (length=12) 4 => string 'string.toupper' (length=14) 5 => string 'string.tolower' (length=14) 6 => string 'string.strip_tags' (length=17) 7 => string 'convert.*' (length=9) 8 => string 'consumed' (length=8) 9 => string 'dechunk' (length=7)
Они позволяют «зазиповать» контент, перевести его в верхний или нижний регистр.
Основные трюки, которые я хотел показать, закончились. Теперь перейдем к квинтэссенции всего, что я умею — к аспектно-ориентированному программированию.
Трюк #7. Аспектно-ориентированное программирование
Посмотрите на этот код и подумайте, хороший он или плохой с вашей точки зрения.
Кажется, код вполне адекватный. Он проверяет права доступа, выполняет логирование, создает юзера, персистит, пытается отловить exception
.
Если посмотреть на всю эту лапшу, она повторяется в каждом нашем методе, и ценное здесь только то, что помечено зеленым.
Все остальное: «secondary concerns» или «crosscutting concerns» — сквозная функциональность.
Обычный ООП не дает возможности взять копипастом все эти конструкции, убрать и куда-то вынести. Это плохо, потому что приходится повторяться. Хотелось бы, чтобы код всегда выглядел чисто и аккуратно.
Чтобы он не содержал логирования (пусть оно как-то само применяется), и не содержал security
.
Чтобы это все, включая логирование…
… и проверку безопасности,…
… выполнялось само.
И это возможно.
Глоссарий «Aspect»
Есть штука, которая называется «Aspect». Это простой пример, который проверяет права доступа. Благодаря ему вы можете видеть некоторую аннотацию, которую еще и подсвечивает плагин для PhpStorm. «Aspect» объявляет SQL-выражения, к каким точкам в коде применять данное условие. Ниже, например, мы хотим для всех публичных методов из класса UserService
применить замыкание.
Замыкание получаем методом $invocation
.
Это некоторая обертка поверх метода reflection
, которая содержит еще аргументы.
Дальше в этом callback для каждого метода можно проверить необходимые права доступа, это называется «Advice».
Мы как бы говорим языку: «Уважаемый PHP, пожалуйста, примени этот метод перед каждым вызовом публичного метода из класса UserService
». Всего лишь одна строчка, а много полезного.
Aspect vs Event Listener
Чтобы было понятнее, я сделал сравнение Aspect с Event Listener. Многие работают в Symfony и знают, что такое Event Dispatcher.
Они похожи в том, что мы передаем какую-то зависимость, например, AutorizationChecker
, и объявляем, куда применять в данном случае. В случае Listener — это SubscrabingEvent
под названием UserCreate
, а в случае Aspect
мы подписываемся на все вызовы публичных методов из UserService
. При этом контент наполнения самого обработчика callback примерно одинаковый: мы просто что-то проверяем и соответственно реагируем.
Рассмотрим, как это все работает под капотом.
Первый этап, который требует аспектный фреймворк, это регистрация Aspects
.
Второй этап. Чтобы это обработать, опять используется предыдущий трюк с PHP-фильтром.
Это специальный компонент, который занимается преобразованием исходного кода. Он это делает скрытно, но работать в продакшн будет хорошо, потому что интегрирован с OPcache.
Третий этап. Все интегрируется на уровне Composer
. Как только устанавливается Go! AOP, он начинает тесно общаться с Composer
и договаривается о том, какие файлы откуда загружать.
Поэтому можно грузить одновременно и версию кода без Aspects
, и с Aspects
. Это можно сделать буквально настройкой в среде.
Дальше начинается довольно сложная матчасть.
PHP-Parser. Чтобы сделать эту сложную работу, провернуть магический трюк, необходимо весь исходный код сперва проанализировать. Хорошо, что есть такая замечательная библиотека Никиты Попова, как PHP-Parser. Она позволяет провести токенизацию и построить абстрактное синтаксическое дерево всего кода.
Четвертый этап. Я создал еще одну библиотеку, которая называется goaop/parser-reflection.
Она работает поверх AST-дерева и позволяет проводить рефлексию исходного кода, не загружая его в память. Для меня это важно, потому что как только класс загружается в память, он оттуда никак не может быть выгружен, а хотелось бы узнать его структуру заранее.
Дальше принимаемся за разбор текущего файла.
Узнаем, какие в нем есть классы и как их изменить, благодаря тому что есть Aspect
.
На выходе получается очень аккуратная и удобная штука в виде простого класса, которая декорирует ваш оригинальный класс.
Особенность фреймворка в том, что он подменяет ваш класс точно таким же классом с таким же именем, а оригинальный при этом переименовывает в кэше. Меняется не ваш оригинальный код — в кэше создается отдельная версия, в которой чуть-чуть меняется название класса, и потом наследуется от этого класса.
Наследование есть даже в том случае, если класс был финальным. Поэтому можно отлавливать и финальные методы, и финальные классы.
Поверх моего фреймворка работает библиотека Aspect MOCK. Она позволяет «замокать», в том числе и финальные методы, и статические методы. Все это работает под капотом.
Наш переопределенный метод выглядит довольно просто — мы вызываем joinPoint
. В терминах аспектно-ориентированного программирования все называется joinPoint
: каждый метод, обращение к свойству, перехват функции или создание.
Что дальше?
Дальше открываются невероятные возможности.
OPcache preloading for AOP Core. Весь AOP-движок будет прекомпилироваться на этапе загрузки приложения. Это позволит снизить накладные расходы на его исполнение с 10 мс до нуля. Bootstrapping фреймворка будет занимать практически ничего, весь фреймворк будет находиться в памяти PHP.
FFI integration to modify binary opcodes. Следующее, что я буду делать, это изменять бинарно опкоды. Как только вы используете PHP-opcodes, в файловой системе генерируется файлик с названием .bin
. При использовании FFI все взлетит.
Modifying PHP engine internal callbacks или модификация PHP-движка со стороны userland. Внутри PHP есть глобальные переменные. Если через FFI подключить PHP сам к себе в userland, то получим доступ к его внутренним свойствам, классам, структурам. Почему бы этим не воспользоваться.
На этом магию остановим, пока не реализуем.
Трюк #8. goaop/framework
Это мой фреймворк, он есть на GitHub и у него там больше тысячи звезд.
composer show goaop/framework --all name : goaop/framework descrip. : Framework for aspect-oriented programming in PHP. keywords : php, aop, library, aspect versions : dev-master, 3.0.x-dev, 2.x-dev, 2.3.1, … type : library license : MIT License
Если вы боитесь магии, я создал помощника в виде плагина для PhpStorm.
Плагин удобен тем, что позволяет подсвечивать синтаксис Pointcuts. Мы знаем, какие хотим методы обрабатывать, и как. Также он предлагает навигацию — подсвечивает подсказки у методов, к тем методам, к которым мы хотим перейти.
Trick #9. Отложенные методы
Напоследок сделаем еще один трюк уже с использованием аспектного фреймворка. Посмотрим, как делать отложенные методы.
Идея довольно проста: есть код, в котором какой-то из методов отрабатывает медленно. В таких случаях рекомендуется вынести выполнение этого кода до момента fastcgi_finish_request
. Мне это кажется неудобным, потому что все время приходится помнить, куда его засунуть, какой-то callback прикрутить — выглядит не нативно.
Что я предлагаю сделать и как это может работать?
Создаем доктриновскую аннотацию, которая называется Deffered
, и помечаем, что она применяется для любого из методов.
После чего создаем Aspect
, который говорит, что вокруг вызова методов, содержащих аннотацию Deffered
, нужно выполнить следующий код.
В свойство Aspect
начинаем накапливать отложенные методы: сохраняем метод, который вызвался, объект, для которого был вызван данный callback, и аргументы, с которыми был вызван callback. Мы не даем выполниться этому коду.
Поклонники React увидят, что тут должен быть promise
. Мы в этот момент пообещаем, что когда-нибудь данный метод будет выполнен, а когда-нибудь потом мы закончим его выполнение, и получим решение.
Посмотрим, как всё это будет работать под капотом.
Регистрируем shutdown_function
прямо в Aspect
. Как только запускается наше приложение, у нас есть callback, который говорит, что после того, как приложение завершится, надо вызвать callback onPhpTerminate
. В этом методе делаем fastcgi_finish_request
и говорим: «Все, отправь, пожалуйста, клиенту весь контент, который создан». И только теперь начнем по одному выполнять отложенные методы.
Для примера представим, что у нас есть некоторый код и синхронный вызов sendPushNotification
.
Допустим, какой-то плохой человек сделал его слишком медленным — он спит 2 с.
Мы не хотим, чтобы клиент, который делает запрос в наше приложение, еще 2 секунды ждал ответа.
Просто помечаем этот метод, как Deferred
.
Код моментально вылетает, клиент сразу получает ответ. Где-то потом в фоновом режиме после завершения запроса, отправляется уведомление, что уже никак не мешает клиенту.