Ломаем паттерн проектирования — Singleton в PHP

в 8:38, , рубрики: design patterns, php, singleton, testing

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

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

Нашел интересную статью, в которой описано, как с помощью бибилотеки dg/bypass-finals можно замокать финальный класс. Этот вариант мне понравился и я попробовал его внедрить. К сожалению, у меня ничего не получилось, так как на проекте используется старая версия PHPUnit.

Поразмыслив, я вспомнил о классе Closure, а конкретно о его статическом методе bind(), который умеет внедрять анонимные функции в контекст нужного объекта какого-либо класса. Больше информации об этом можно найти в официальной документации. Поэтому я создал трейт, который использовал в своих тестах (может кому-то тоже будет полезен)

trait PrivatePropertySetterTrait
{
    protected function assignValue($object, string $attribute, $value)
    {
        $setter = function ($value) use ($attribute) {
            $this->$attribute = $value;
        };

        $setterClosure = Closure::bind($setter, $object, get_class($object));
        $setterClosure($value);
    }
}

Данный трейт принимает объект класса, название свойства, куда нужно установить значение и, собственно, само значение. Далее объявляется простая анонимная функция, которая с помощью указателя $this присваивает полученное значение в свойство класса. Дальше в бой идёт класс Closure с его статическим методом bind(). Метод принимает объект класса, анонимную функцию, описанную выше, и полное имя класса. Таким образом, анонимная функция внедряется в контекст объекта и метод bind() возвращает нам объект класса Closure, который мы можем вызвать как обычную функцию, потому как он определяет магический метод __invoke(). И вуаля!

В итоге мне удалось решить мою проблему, и тогда я вспомнил о шаблоне проектирования Singleton. Получится ли таким же способом внедрить анонимную функцию, которая будет создавать новые объекты класса? Конечно же я пошёл это проверять!

Написав небольшой кусок кода

Песочница с кодом

<?php

final class Singleton
{
    private static $instance;

    public static function getInstance()
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    private function __construct()
    {
    }

    private function __clone()
    {
    }

    private function __wakeup()
    {
    }
}

$s1 = Singleton::getInstance();
var_dump(spl_object_id($s1));

$createNewInstance = function () {
    return new self();
};
$newInstanceClosure = Closure::bind($createNewInstance, $s1, Singleton::class);

$s2 = $newInstanceClosure();
var_dump(spl_object_id($s2));

который работает по такому же принципу, только вместо присвоения значения свойству класса — создаётся новый объект с помощью оператора new. Функция spl_object_id() возвращает уникальный идентификатор объекта. Больше информации об этой функции можно найти в документации. С помощью spl_object_id() и var_dump() вывожу уникальные идентификаторы объектов и вижу то что они отличаются! Мне всё же удалось подтвердить эту теорию и создать новый екземпляр Singleton класса!

В этой статье я хотел поделиться с сообществом PHP моей весьма любопытной находкой.

Спасибо за внимание!

Автор: greeflas

Источник

* - обязательные к заполнению поля