Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение3 мин
Количество просмотров1.3K

Цель

Подмена (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 пользователей.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 12: ↑12 и ↓0+12
Комментарии4

Публикации

Истории

Работа

PHP программист
104 вакансии

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань