Школа магии PHP
https://habr.com/ru/company/oleg-bunin/blog/478618/Что такое магия в PHP? Обычно под этим подразумевают методы вроде _construct() или __get(). Магические методы в 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 вы попытаетесь провалиться внутрь этого класса, он моментально остановит выполнение вашей команды. Не хочу, чтобы вы копались в этом коде — он слишком опасный. Он будет предупреждать, что тут делать нечего.
Часть 2 тут
https://telegra.ph/SHkola-magii-PHP-CHast-2-12-04