Школа магии PHP

в 10:16, , рубрики: php, phprussia, аспектно-ориентированное, Блог компании Конференции Олега Бунина (Онтико), магические выражения, магия, магия php, ооп, Программирование, Разработка веб-сайтов

Что такое магия в PHP? Обычно под этим подразумевают методы вроде _construct() или __get(). Магические методы в PHP — это лазейки, которые помогают разработчикам выполнять удивительные вещи. В сети полно инструкций по их использованию, с которыми вы наверняка знакомы. Но что если мы скажем, что вы даже не видели настоящую магию? Ведь, чем больше вам кажется, что вы знаете все, тем больше магии ускользает от вас.

Школа магии PHP - 1

Давайте отбросим установленные рамки правил ООП и сделаем невозможное возможным в школе магии PHP. Главный и первый волшебный преподаватель школы — Александр Лисаченко (NightTiger). Он научит магическому мышлению и, возможно, вы полюбите магические методы, нестандартные способы доступа к свойствам, изменение контекстов, аспектно-ориентированное программирование и потоковые фильтры.


Александр Лисаченко — руководитель отдела веб-разработки и архитектуры в Альпари. Автор и ведущий разработчик аспектно-ориентированного фреймворка Go! AOP. Докладчик на международных конференциях по PHP.

В хорошем фильме «Иллюзия обмана» есть фраза:

«Чем вы ближе, тем меньше вы видите».

Это же можно сказать о PHP, как о магическом трюке, который позволяет проворачивать необычные вещи. Но прежде всего он создан, чтобы вас обмануть: «...an action that is intended to deceive, either as a way of cheating someone, or as a joke or form of entertainment».

Если мы возьмем PHP и вместе попытаемся на нем написать что-то магическое, скорее всего, я вас обману. Я проверну какой-нибудь трюк, и вы будете долго гадать, почему так происходит. Все потому, что PHP — это известный своими необычными штуками язык программирования.

Магическое снаряжение

Что нам потребуется из магического снаряжения? Знакомые до боли методы.

__construct(), __destruct(), __clone(),
__call(), __callStatic(), 
__get(), __set(), __isset(), __unset(),
__sleep(), __wakeup(), 
__toString(), __invoke(), __set_state(),
__debugInfo()

Последний метод отмечу отдельно — с ним можно проворачивать необычные вещи. Но это не все.

  • declare(ticks=1).
  • debug_backtrace(). Это наш спутник, чтобы понять, где мы находимся в текущем коде, посмотреть, кто и зачем нас вызвал, с какими аргументами. Пригодится, чтобы принять решение не выполнять всю логику.
  • unset(), isset(). Кажется, что ничего особенного, но эти конструкции скрывают много трюков, которые рассмотрим дальше.
  • by-reference passing. Когда мы передаем какой-то объект или переменную по ссылке, то стоит ожидать, что с вами неизбежно произойдет какая-нибудь магия.
  • bound closures. На замыканиях и том, что они могут биндиться, можно построить массу трюков.
  • Reflection API помогает вывести рефлексию на новый уровень.
  • StreamWrapper API.

Снаряжение готово — напомню первое правило магии.

Всегда будь самым умным парнем в комнате.

Трюк #1. Невозможное сравнение

Начнем с первого трюка, который я называю «Невозможное сравнение».
Посмотрите внимательно на код и подумайте, может ли такое произойти в PHP.

Школа магии PHP - 2

Есть переменная, объявляем её значение, а потом она внезапно сама себе не равна.

Not-a-number

Есть такой волшебный артефакт, как NaN — Not-a-number.

Школа магии PHP - 3

Его удивительная особенность в том, что он сам себе не равен. И в этом наш первый трюк: использовать NaN, чтобы озадачить товарища. Но NaN не единственное решение для этой задачи.

Используем константы

Школа магии PHP - 4

Фишка в том, что мы можем для namespace объявить false как true и сравнить. Незадачливый разработчик долго будет гадать, почему там true, а не false.

Обработчик

Следующий трюк — артиллерия помощнее, чем два предыдущих варианта. Рассмотрим, как он работает.

Школа магии PHP - 5

Трюк базируется на tick_function и, как я уже упоминал, declare(ticks=1).

Как это всё работает из коробки? Сперва объявляем некоторую функцию, и она по ссылке принимает параметр isMagic, а дальше пытается поменять это значение на true. После того, как мы объявили declare(ticks=1), интерпретатор PHP после каждой элементарной операции вызывает register_tick_function — callback. В этом callback мы можем то значение, которое было раньше false, поменять на true. Магия!

Трюк #2. Магические выражения

Возьмем пример, в котором объявлены две переменные. Одна из них false, другая true. Делаем isBlack и isWhite и var_dump’аем результат. Как вы думаете, что будет в итоге?

Школа магии PHP - 6

Приоритет операторов. Правильный ответ false, потому что в PHP есть такое понятие, как «приоритет операторов».

Школа магии PHP - 7

Удивительно, но у логического оператора or приоритет меньше, чем у операции присваивания. Поэтому происходит просто присваивание false. В isWhite может быть любое другое выражение, которое выполнится, если первая часть не отработает.

Магические выражения

Посмотрите на код ниже. Есть некоторый класс, который содержит конструктор, и некоторая фабрика, код которой будет далее.

Школа магии PHP - 8

Обратите внимание на последнюю строчку.

$value = new $factory->build(Foo::class);

Есть несколько вариантов, что может произойти.

  • $factory будет использовано как имя класса new;
  • будет ошибка парсинга;
  • будет использоваться вызов $factory->build(), значение которого вернет эта функция, в результате получится new;
  • будет использовано значение свойства $factory->build, чтобы сконструировать класс.

Давайте проверим последнюю идею. В классе $factory объявим ряд магических функций. Они будут писать, что мы делаем: вызываем свойство, обращаемся к методу, или вообще пытаемся вызвать invoke у объекта.

class Factory
{
    public function _get($name) {echo "Getting property {$name}"; return Foo::class;}
    public function _call($name, $arg) {echo "Calling method {$name}"; return Foo::class;}
    public function _invoke($name) {echo "Invoking {$name}"; return Foo::class;}
}

Правильный ответ: мы вызываем не метод, а свойство. После $factory->build находится параметр для конструктора, который мы передадим в этот класс.

Кстати, в этом фреймворке у меня реализована фишка, которая называется «перехват создание новых объектов» — можно «замокать» эту конструкцию.

Лазейка в парсере

Следующий пример касается самого PHP-парсера. Пробовали ли вы когда-нибудь вызывать функции или присваивать переменные внутри фигурных скобок?

Школа магии PHP - 9

Этот трюк мне попался в Twitter, он работает крайне нестандартно.

$result = ${'_' . !$_=getCallback()}();

$_=getCallback();                // string(5) "hello"
!$_=getCallback()}();           // bool(false)
'_'.!$_=getCallback()}();      // string(1) "_"
${'_'.!$_=getCallback()}();  // string(5) "hello"

Сперва переменной с названием _ (подчеркивание) мы присваиваем значение выражения. У нас уже есть переменная, мы пытаемся логически инвертировать ее значение, и получаем false — строка кастуется как бы к true. Дальше это всё склеиваем в названии переменной, через которую потом обращаемся уже внутри фигурных скобок.

Что такое магия? Это развлечение, которое позволяет нам почувствовать себя воодушевленно, необычно, сказать: «Что? Так можно было?!»

Трюк #3. Ломаем правила

Мне нравится в PHP то, что можно ломать правила, которые все создают, пытаясь быть суперзащищенными. Есть конструкция под названием «запечатанный класс», у которого приватный конструктор. Ваша задача как ученика мага создать экземпляр этого класса.

Школа магии PHP - 10

Рассмотрим три варианта, как это можно сделать.

Обходной путь

Первый путь самый очевидный. Он должен быть знаком каждому разработчику — это стандартный API, который нам предлагает язык.

Школа магии PHP - 11

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

Вариант рабочий, простой, не требует какого-то пояснения.

Замыкание

Второй вариант требует уже больше внимания и умения. Создается анонимная функция, которая затем биндится к скопу того класса.

Школа магии PHP - 12

Здесь мы находимся внутри класса и можем спокойно вызывать приватные методы. Этим и пользуемся, вызывая new static из контекста нашего класса.

Школа магии PHP - 13

Десериализация

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

Школа магии PHP - 14

Если в определенном формате написать определенную строчку, подставить туда определенные значения — получится наш класс.

Школа магии PHP - 15

После десериализации получим наш instance.

doctrine/instantiator package

Магия часто становится документированным фреймворком или библиотекой — например, в doctrine/instantiator все это реализовано. Мы можем создавать любые объекты с любым кодом.

composer show doctrine/instantiator --all
name     : doctrine/instantiator
descrip. : A small, lightweight utility to instantiate objects in PHP without invoking their constructors
keywords : constructor, instantiate
type     : library
license  : MIT License (MIT)

Трюк #4. Intercepting property access

Тучи сгущаются: класс секретный, свойства и конструктор приватные, и еще callback.

class Secret
{
    private $secret = 42;
    private function _construct()
    {
        echo 'Secret is: ', $this->secret;
    }

    private function onPropAccess(string $name)
    {
        echo "Accessing property {$name}";

        return 100500;
    }
}
// How to create a secret instance and intercept the secret value?

Наша задача, как волшебников, как-то вызвать callback.

Добавляем магический… getter

Добавим щепотку магии, чтобы все это заработало.

Школа магии PHP - 16

Эта щепотка магии — магический getter. Он вызывает функцию, и пока что ничего страшного не произошло. Но воспользуемся предыдущим трюком и создадим экземпляр этого объекта в обход приватного конструкта.

Школа магии PHP - 17

Теперь надо каким-то образом вызвать callback.

«Unset» внутри замыкания

Чтобы сделать это, создадим замыкание. Внутри замыкания, которое находится в скопе класса, удалим функцией unset() эту переменную.

Школа магии PHP - 18

unset позволяет временно исключить переменную, что позволит вызываться нашему магическому методу get.

Вызываем приватный конструктор

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

Школа магии PHP - 19

Так наш секретный класс рассыпался в пух и прах.

Школа магии PHP - 20

Мы получили сообщение о том, что мы:

  • перехватили;
  • вернули что-то совершенно другое.

leedavis/altr-ego package

Много магии уже задокументировано. Пакет altr-ego как раз притворяется вашим компонентом.

composer show leedavis81/altr-ego --all
name     : leedavis81/altr-ego
descrip. : Access an objects protected / private properties and methods
keywords : php, break scope
versions : dev-master, v1.0.2, v1.0.1, v1.0.0
type     : library
license  : MIT License (MIT)

Вы можете создать один свой объект и прицепить к нему второй. Это позволит проводить изменения объекта. Он будет изменяться послушно и выполнять все ваши пожелания.

Трюк #5. Immutable objects в PHP

Существуют ли в PHP Immutable object? Да, причем очень и очень давно.

namespace Magic
{
    $object = (object) [
        "Secretproperty" => 'test'
    ];
    var_dump($object);
}

Только получать их надо интересным образом. Интересность в том, что мы создаем массив, у которого есть специальный ключ. Он начинается с конструкции — это нулевой байт-символ, и после Secret мы тоже видим .

Конструкция используется в PHP, чтобы объявить приватное свойство внутри класса. Если мы попытаемся кастануть какой-то объект к массиву, увидим те же самые ключи. У нас появится не что иное, как stdClass. Он содержит в себе приватное свойство из класса Secret, которое равно test.

object(stdClass) [1]
    private 'property' (Secret) => string 'test' (length=4)

Единственная незадача — потом достать это свойство оттуда никак нельзя. Оно создается, но недоступно.

Я подумал, что это довольно неудобно — у нас есть Immutable objects, но использовать его нельзя. Поэтому решил, что пора бы запилить свое решение. Я использовал все мои знания и магию, которая имеется в PHP, чтобы создать конструкцию на базе всех наших магических трюков.

Начнем с простого — создадим DTO и попытаемся в ней перехватить все свойства (см. предыдущий трюк).

Сохраним в надежном месте значения, которые оттуда захватим. Они будут недоступы никакими методами: ни reflection, ни замыканиями, ни другой магией. Но возникает неопределенность — существует ли такое место в PHP, которое бы позволяло гарантированно сохранять какие-то переменные, чтобы туда никакой хитрый юный программист вообще не добрался?

Предоставим магический метод, чтобы можно было прочитать это значение. Для этого у нас есть магические getters, магические методы isset, которые позволяют предоставить API.

Вернемся к надежному месту и попробуем поискать.

  • Global variables отметаются — любой желающий может их поменять.
  • Public properties тоже не подходят.
  • Protected properties так себе, потому что дочерний класс проберется.
  • Private properties. Нет доверия, потому что через замыкание или через reflection его можно поменять.
  • Private static properties можно попробовать, но тоже ломается reflection.

Казалось бы, спрятать значения переменных некуда. Но нашлась волшебная штука — Static variables in functions — это переменные, которые находятся внутри функций.

Безопасное хранение значений

Я спросил на специальном канале Stack Overflow у Никиты Попова и Джея Воткинса об этом.

Школа магии PHP - 21

Это функция, внутри которой объявлена статичная переменная. Можно ли из неё как-то достать, поменять? Ответ — нельзя.

Мы нашли маленькую лазейку в потусторонний мир защищенных переменных и хотим использовать её.

Передача значений по ссылкам

Использовать будем нестандартно, как свойство объекта. Но нельзя передавать свойство, поэтому используем классическую передачу значений по ссылкам.

Школа магии PHP - 22

Получается, есть класс, в котором есть магический метод callStatic, и в нем объявлена переменная Static. В любой вызов какой-то функции мы передаем значение переменной из Immutable object по ссылке во все наши вложенные методы. Так мы как бы предоставляем контекст.

Сохраняем state

Посмотрим, как сохраняется состояние.

Школа магии PHP - 23

Всё довольно просто. Для переданного объекта пользуемся функцией spl_object_id, которая для каждого экземпляра возвращает отдельный идентификатор. Тот State, который мы уже достали из объекта, пытаемся сохранить туда. Ничего особенного.

Применяем состояние объекта

Здесь опять есть конструкция передачи значений по ссылке и unset свойства. Мы unset’им все текущие свойства, предварительно их сохранив в переменную State, и устанавливаем этот контекст для объекта. Больше объект не содержит никаких свойств, а только свой идентификатор, который объявляется с помощью spl_object_id и привязан к этому объекту, пока жив.

Школа магии PHP - 24

Получаем State

Дальше все просто.

Школа магии PHP - 25

На магический getter достаем этот контекст и вызываем из него наше свойство. Теперь никто и ничто не может поменять значение после того, как подключен этот Trait.

Школа магии PHP - 26

Все волшебные методы переопределены и реализуют неизменяемость объекта.

Школа магии PHP - 27

lisachenko/immutable-object

Как положено, все сразу оформляется в библиотеку и готово к использованию.

composer show /immutable-object --all
name     : /immutable-object
descrip. : Immutable object library
keywords : 
versions : * dev-master
type     : library
license  : MIT License (MIT)

Выглядит библиотека довольно просто. Подключаем ее и создаем наш класс. У него разные свойства: приватные, протектные и public. Подключаем ImmutableTrait.

Школа магии PHP - 28

После этого можно инициировать объект один раз — посмотреть его значение. Оно действительно там сохраняется и даже выглядит, как настоящая DTO.

object (MagicObject) [3]
    public 'value' => int 200

Но если мы попытаемся ее поменять, например, так…

Школа магии PHP - 29

… то тут же сразу получим fatal exception. Мы не можем менять свойство, потому что оно Immutable. Как же так?

Школа магии PHP - 30

Если ввязаться в увлекательный челлендж и попытаться её дебажить, то получится следующее.

Школа магии PHP - 31

Это мой подарочек. Как только в PHPStorm вы попытаетесь провалиться внутрь этого класса, он моментально остановит выполнение вашей команды. Не хочу, чтобы вы копались в этом коде — он слишком опасный. Он будет предупреждать, что тут делать нечего.

Трюк #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. Аспектно-ориентированное программирование

Посмотрите на этот код и подумайте, хороший он или плохой с вашей точки зрения.

Школа магии PHP - 32

Кажется, код вполне адекватный. Он проверяет права доступа, выполняет логирование, создает юзера, персистит, пытается отловить exception.

Если посмотреть на всю эту лапшу, она повторяется в каждом нашем методе, и ценное здесь только то, что помечено зеленым.

Школа магии PHP - 33

Все остальное: «secondary concerns» или «crosscutting concerns» — сквозная функциональность.

Обычный ООП не дает возможности взять копипастом все эти конструкции, убрать и куда-то вынести. Это плохо, потому что приходится повторяться. Хотелось бы, чтобы код всегда выглядел чисто и аккуратно.

Школа магии PHP - 34

Чтобы он не содержал логирования (пусть оно как-то само применяется), и не содержал security.

Школа магии PHP - 35

Чтобы это все, включая логирование…

Школа магии PHP - 36

… и проверку безопасности,…

Школа магии PHP - 37

… выполнялось само.

И это возможно.

Глоссарий «Aspect»

Есть штука, которая называется «Aspect». Это простой пример, который проверяет права доступа. Благодаря ему вы можете видеть некоторую аннотацию, которую еще и подсвечивает плагин для PhpStorm. «Aspect» объявляет SQL-выражения, к каким точкам в коде применять данное условие. Ниже, например, мы хотим для всех публичных методов из класса UserService применить замыкание.

Школа магии PHP - 38

Замыкание получаем методом $invocation.

Школа магии PHP - 39

Это некоторая обертка поверх метода reflection, которая содержит еще аргументы.

Дальше в этом callback для каждого метода можно проверить необходимые права доступа, это называется «Advice».

Школа магии PHP - 40

Мы как бы говорим языку: «Уважаемый PHP, пожалуйста, примени этот метод перед каждым вызовом публичного метода из класса UserService». Всего лишь одна строчка, а много полезного.

Aspect vs Event Listener

Чтобы было понятнее, я сделал сравнение Aspect с Event Listener. Многие работают в Symfony и знают, что такое Event Dispatcher.

Школа магии PHP - 41

Они похожи в том, что мы передаем какую-то зависимость, например, AutorizationChecker, и объявляем, куда применять в данном случае. В случае Listener — это SubscrabingEvent под названием UserCreate, а в случае Aspect мы подписываемся на все вызовы публичных методов из UserService. При этом контент наполнения самого обработчика callback примерно одинаковый: мы просто что-то проверяем и соответственно реагируем.

Рассмотрим, как это все работает под капотом.

Первый этап, который требует аспектный фреймворк, это регистрация Aspects.

Школа магии PHP - 42

Второй этап. Чтобы это обработать, опять используется предыдущий трюк с PHP-фильтром.

Школа магии PHP - 43

Это специальный компонент, который занимается преобразованием исходного кода. Он это делает скрытно, но работать в продакшн будет хорошо, потому что интегрирован с OPcache.

Третий этап. Все интегрируется на уровне Composer. Как только устанавливается Go! AOP, он начинает тесно общаться с Composer и договаривается о том, какие файлы откуда загружать.

Школа магии PHP - 44

Поэтому можно грузить одновременно и версию кода без Aspects, и с Aspects. Это можно сделать буквально настройкой в среде.

Дальше начинается довольно сложная матчасть.

PHP-Parser. Чтобы сделать эту сложную работу, провернуть магический трюк, необходимо весь исходный код сперва проанализировать. Хорошо, что есть такая замечательная библиотека Никиты Попова, как PHP-Parser. Она позволяет провести токенизацию и построить абстрактное синтаксическое дерево всего кода.

Школа магии PHP - 45

Четвертый этап. Я создал еще одну библиотеку, которая называется goaop/parser-reflection.

Школа магии PHP - 46

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

Дальше принимаемся за разбор текущего файла.

Школа магии PHP - 47

Узнаем, какие в нем есть классы и как их изменить, благодаря тому что есть Aspect.

Школа магии PHP - 48

На выходе получается очень аккуратная и удобная штука в виде простого класса, которая декорирует ваш оригинальный класс.

Школа магии PHP - 49

Особенность фреймворка в том, что он подменяет ваш класс точно таким же классом с таким же именем, а оригинальный при этом переименовывает в кэше. Меняется не ваш оригинальный код — в кэше создается отдельная версия, в которой чуть-чуть меняется название класса, и потом наследуется от этого класса.

Школа магии PHP - 50

Наследование есть даже в том случае, если класс был финальным. Поэтому можно отлавливать и финальные методы, и финальные классы.

Поверх моего фреймворка работает библиотека 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.

Школа магии PHP - 51

Плагин удобен тем, что позволяет подсвечивать синтаксис Pointcuts. Мы знаем, какие хотим методы обрабатывать, и как. Также он предлагает навигацию — подсвечивает подсказки у методов, к тем методам, к которым мы хотим перейти.

Trick #9. Отложенные методы

Напоследок сделаем еще один трюк уже с использованием аспектного фреймворка. Посмотрим, как делать отложенные методы.

Идея довольно проста: есть код, в котором какой-то из методов отрабатывает медленно. В таких случаях рекомендуется вынести выполнение этого кода до момента fastcgi_finish_request. Мне это кажется неудобным, потому что все время приходится помнить, куда его засунуть, какой-то callback прикрутить — выглядит не нативно.

Что я предлагаю сделать и как это может работать?

Создаем доктриновскую аннотацию, которая называется Deffered, и помечаем, что она применяется для любого из методов.

Школа магии PHP - 52

После чего создаем Aspect, который говорит, что вокруг вызова методов, содержащих аннотацию Deffered, нужно выполнить следующий код.

Школа магии PHP - 53

В свойство Aspect начинаем накапливать отложенные методы: сохраняем метод, который вызвался, объект, для которого был вызван данный callback, и аргументы, с которыми был вызван callback. Мы не даем выполниться этому коду.

Поклонники React увидят, что тут должен быть promise. Мы в этот момент пообещаем, что когда-нибудь данный метод будет выполнен, а когда-нибудь потом мы закончим его выполнение, и получим решение.

Посмотрим, как всё это будет работать под капотом.

Школа магии PHP - 54

Регистрируем shutdown_function прямо в Aspect. Как только запускается наше приложение, у нас есть callback, который говорит, что после того, как приложение завершится, надо вызвать callback onPhpTerminate. В этом методе делаем fastcgi_finish_request и говорим: «Все, отправь, пожалуйста, клиенту весь контент, который создан». И только теперь начнем по одному выполнять отложенные методы.

Для примера представим, что у нас есть некоторый код и синхронный вызов sendPushNotification.

Школа магии PHP - 55

Допустим, какой-то плохой человек сделал его слишком медленным — он спит 2 с.

Школа магии PHP - 56

Мы не хотим, чтобы клиент, который делает запрос в наше приложение, еще 2 секунды ждал ответа.

Просто помечаем этот метод, как Deferred.

Школа магии PHP - 57

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

На этом все. Надеюсь, что вам понравился урок магии и натолкнул вас на какие-то размышления. Буду признателен, если оставите комментарии.

Следующая профессиональная конференция PHP Russia 2020 пройдет в мае. Мы готовим новую концепцию и принимаем доклады — подавайте заявки. Подписывайтесь на рассылку и telegram-канал, чтобы раньше других получить приглашение на PHP Russia 2020.

Автор: Олег Бунин

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js