Цель
Подмена (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!
