Школа магии PHP. Часть 2

Школа магии 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.




Код моментально вылетает, клиент сразу получает ответ. Где-то потом в фоновом режиме после завершения запроса, отправляется уведомление, что уже никак не мешает клиенту.

Report Page