Подмена функций стандартной библиотеки PHP с помощью xepozz/internal-mocker

от автора

Цель

Подмена (mock) функций, которые уже “загружены” в PHP еще до подгрузки Composer Autoloader, каких-либо include или других объвлений function name() {}

Подмена не только из под не пустого namespace, например App\Service\name , но и из корневого namespace: проще всего это сделать через use function name;

Проблема

Если объявить функцию с именем, которая уже существует в стандартной библиотеке PHP, то получим ошибку, что такая функция уже существует и переопределить её нельзя.

Можно было бы “выгрузить” её из памяти, но увы, выгружать из памяти функции в PHP нельзя.

Можно лишь переопределить функцию до её непосредственного объявления. Но такой способ не подходит, потому что функция уже объявлена при любом вызове php .

Ресерч

В php.ini можно найти флаг disable_functions , которая принимает список имен функций, которые нужно “не объявлять” в недрах PHP.

Если использовать этот флаг, то php -ddisable_functions=time -r "echo time();" выкинет ошибку:

❯ php -ddisable_functions=time -r "echo time();" PHP Fatal error:  Uncaught Error: Call to undefined function time() in Command line code:1 Stack trace: #0 {main}   thrown in Command line code on line 1  Fatal error: Uncaught Error: Call to undefined function time() in Command line code on line 1  Error: Call to undefined function time() in Command line code on line 1  Call Stack:     0.0000     389568   1. {main}() Command line code:0

Это и логично. Функции time больше нет. Но теперь ведь можно создать её самостоятельно?

Если объявить функцию самостоятельно, то ошибки больше не будет:

❯ php -ddisable_functions=time -r "function time() { return 123; } echo time();" 123% 

Бинго!

Помещаем объявление функции в библиотеку, создаем State manager, через которого сможем управлять возвращаемым значение “123” и делаем пользователю интерфейс взаимодействия с этим менеджером.

Теперь, если пользователь захочет протестировать вызов time , то сможем самостоятельно указать требуемые значения. Время в будущем, в прошлом, 0, false, что угодно.

Но как быть, если нужно протестировать измененную функцию time лишь в одном тесте, а в других местах оставить всё как есть?

Можно так и сделать: State manager создает для всех тестов функцию, которая эмулирует стандартную time , а в нужном тесте наложить на общую эмуляцию частную.

Вроде всё логично и понятно. Можно накодить и наслаждаться тестированием.

Однако, а как эмулировать системное время? Если с различными полифилами от symfony всё понятно: можно создать какую-то функцию, которая будет базировать на другой функции, преобразовывать результат под новый формат и отдавать его.

Но на какой функции нужно базировать время?

DateTime* классы? date() ? mktime ? hrtime ? А если их тоже отключить нужно?

Bash! ?

PHP имеет возможность в любое время обратиться к своему старшему брату-башу простыми обратными кавычками: command . Результат будет строкой, но всегда можно «кастануть».

Для аналога time() команда date +%s .

Значит для State manager осталось написать только возможность использовать не статичное значение, а функцию, которая каждый раз будет выполняться.

Всё это и не только сделано в библиотеке xepozz/internal-mocker

Читаем доку по установке и первичной настройке, добавляем нужные файлы, вписываем следующую конфигурацию:

<?php  $mocker = new Mocker(); $mocker->load([     [         'namespace' => '',         'name' => 'time',         'function' => fn () => `date +%s`,     ], ]); MockerState::saveState(); 

И получаем сгенерированный mock для функции time , который просто будет работать всегда как обычный time в самом PHP

<?php  namespace {     use Xepozz\InternalMocker\MockerState;      function time(...$arguments)     {         if (MockerState::checkCondition(__NAMESPACE__, "time", $arguments)) {             return MockerState::getResult(__NAMESPACE__, "time", $arguments);         }         return MockerState::getDefaultResult(__NAMESPACE__, "time", fn () => `date +%s`);     } } 

А протестить это очень легко:

<?php  namespace Xepozz\InternalMocker\Tests\Integration;  use PHPUnit\Framework\TestCase; use Xepozz\InternalMocker\MockerState;  use function time;  final class TimeTest extends TestCase {     public function testRun()     {         $this->assertEquals(`date +%s`, time());     }      public function testRun2()     {         MockerState::addCondition(             '',             'time',             [],             100         );          $this->assertEquals(100, time());     }      public function testRun3()     {         $this->assertEquals(`date +%s`, time());     }      public function testRun4()     {         $now = time();         sleep(1);         $next = time();          $this->assertEquals(1, $next - $now);     } }

Если кто-то писал свои костыли или специально убирал use function из файлов, чтобы подменять функции в нужном namespace, теперь можете избавиться от них и заменить это на подключение библиотеки и небольшой конфиг.


Полезные ссылки

Описание disable-functions: https://www.php.net/manual/en/ini.core.php#ini.disable-functions

Internal mocker: https://github.com/xepozz/internal-mocker/

А этот пост был написан еще неделю назад в моём телеграмме: https://t.me/handle_topic ?

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

Приходилось ли сталкиваться с подменой стандартных функций в PHP?

22.58% Да7
77.42% Нет24

Проголосовал 31 пользователь. Воздержались 5 пользователей.

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

Считаешь ли инструмент полезным?

62.96% Да17
37.04% Нет10

Проголосовали 27 пользователей. Воздержались 8 пользователей.

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

Будешь использовать у себя?

9.52% Да2
90.48% Нет19

Проголосовал 21 пользователь. Воздержались 12 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/797343/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *