company_banner

Школа магии PHP

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



    Давайте отбросим установленные рамки правил ООП и сделаем невозможное возможным в школе магии 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.



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

    Not-a-number


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



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

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




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

    Обработчик


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



    Трюк базируется на 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’аем результат. Как вы думаете, что будет в итоге?



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



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

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


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



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

    $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-парсера. Пробовали ли вы когда-нибудь вызывать функции или присваивать переменные внутри фигурных скобок?



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

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

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

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

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


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



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

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


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



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

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

    Замыкание


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



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



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


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



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



    После десериализации получим наш 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


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



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



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

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


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



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

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


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



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



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

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

    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) [
            "\0Secret\0property" => 'test'
        ];
        var_dump($object);
    }

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

    Конструкция используется в 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 у Никиты Попова и Джея Воткинса об этом.



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

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

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


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



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

    Сохраняем state


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



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

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


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



    Получаем State


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



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



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



    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.



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

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

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



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



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



    Это мой подарочек. Как только в 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. Аспектно-ориентированное программирование


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



    Кажется, код вполне адекватный. Он проверяет права доступа, выполняет логирование, создает юзера, персистит, пытается отловить 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.



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

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

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

    Комментарии 14

      +25
      Представляю, сколько людей прочитав эту статью подумало: «О, отличные примеры для собеседований!». Ведь это так здорово, спрашивать программиста как отработает код, за который в реальном проекте надо сразу увольнять. А так — повод потешить свое самолюбие, и посмотреть как люди с 10+ летним опытом не знают «элементарных вещей».
        +3
        Данный материал содержит не очень очевидные возможности языка. Обычно к ним относятся с насмешкой: «опять этот г… код», «повод потешить свое самолюбие» и т.д Тут нет действительно ничего нового для меня.
        Однако именно из таких находок появляются иногда интересные технические решения и библиотеки. Такие как ocramius/proxy-manager, goaop/framework и другие. Поэтому иногда приходится покопаться в мусоре чтобы отыскать тот самый бриллиант что там лежит.

        Моя команда с увлечением решала нестандартные задачки, а теперь это высококвалифицированные специалисты, работающие в крупных известных компаниях.

        Само-собой, не надо спрашивать такие вопросы на собеседованиях ) Но вот заинтересоваться в примерах самому и попробовать сделать что-то свое интересное — всегда можно. Главное — не останавливаться на достигнутом.

        А что касательно самой презентации и видео насчет магии — это да, здесь был маркетинговый аспект, нацеленный на подачу именно во время конференции, поэтому прошу прощения за некоторое зазнайство — это было нужно чтобы достичь правильной подачи доклада.
        +1

        Отличная статья. Спасибо, прочёл с удовольствием


        Меня заинтересовал ваш framework. Допустим, у меня есть код:


        /**
         * @Before("execution(public UserService->*(*))")
         * @param MethodInvocation $invocation
         * 
         * @throws AccessDeniedException
         */
        public function checkPermission(MethodInvocation $invocation) {
            if(!$this->security->isGranted('ROLE_USER')) {
                throw new AccessDeniedException();
            }
        
            // ...
        }

        … который мне нужно применить не только к одному конкретному классу (в данном случае UserService), а сразу к группе файлов или директории/пространству имён. Это возможно?

          0
          Да, конечно возможно. Используйте двойную звездочку для указания любого неймспейса и одинарную для указания компонента. Например, execution(public Acme\**\Service*->*(*)) — будет совпадать со всеми вызовами публичных методов классов из неймспейса Acme на любом уровне вложенности и имя которых начинается с префикса Service.

          Дополнительные примеры можно глянуть из теста: PointcutParserTest фреймворка.
            0
            а можно как-то подвязаться на вызов методов у классов реализующих определенный интерфейс?
              0
              Да, рядом с указанием вызова методов execution() нужно будет дописать еще && within(YourInterfaceName+) — это сматчится со всеми вызовами методов определенных в классах, реализующих заданный интерфейс YourInterfaceName.

              Смотрите примеры грамматики поинткатов в тесте )
              0

              Здорово! Большое спасибо за разъяснение

            +1

            Спасибо. Очень пригодится, если придется разбирать чей-то легаси-код с подобными трюками.

              0

              Далеко не факт, что такие трюки будут именно в легаси. Тот же биндинг замыканий я часто использую для гидрации/дегидрации объектов в ORM-like задачах. Да и для тестов бывает удобно, пускай они и хрупкими получаются.

              +3

              Да, PHP пропитан магией! Главное не превратить свой код в diablo. Где в конце придется победить главного босса...

                +4

                Самого себя.

                0

                У меня синдром самозванца чуть не начался после прочтения, особенно про AOP :)
                А где-то полгода назад я впервые услышал о мутационном тестировании php на конференции.
                При этом всем я уже довольно приличное время в веб разработке.
                Видимо веб такая штука, когда в принципе появляется много чего каждый год, но все это решает часто не существующие проблемы, либо уже решённые. В итоге ты учишь много чего, а сделать можешь в основном то, что и 5-8 лет назад.

                  +2
                  У меня синдром самозванца чуть не начался после прочтения, особенно про AOP :)
                  А где-то полгода назад я впервые услышал о мутационном тестировании php на конференции.
                  При этом всем я уже довольно приличное время в веб разработке.

                  Видимо веб такая штука, когда в принципе появляется много чего каждый год,

                  «Mutation testing was originally proposed by Richard Lipton as a student in 1971» © wiki. И веб тут не причём.
                  Пора бы уже перестать пиарить миф о том что в разработке всё кардинально меняется каждый год. Большинство идей тянутся из прошлого века.

                  но все это решает часто не существующие проблемы, либо уже решённые

                  Не существующие у вас или не существующие ни у кого в индустрии?
                  Если первое то не показатель, а если второе то сильно громкое заявление.

                  А случаи решения несуществующих проблем существуют — да, но относятся к конкретным проектам. Design-by-buzzword, то есть попытка применять архитектурные ограничения без чёткого понимания их полезности процветает в индустрии.
                    +1
                    когда в принципе появляется много чего каждый год

                    https://github.com/goaop/framework/releases/tag/1.0.0 — 2016, а вообще проекту лет 6


                    В итоге ты учишь много чего, а сделать можешь в основном то, что и 5-8 лет назад.

                    Подразумевается, что учишь для того, чтобы сделать это эффективней чем до начала этой учёбы.

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

                  Самое читаемое