Исправляем паттерн проектирования — Singleton в PHP

    Недавно я писал о том, как сломать паттерн проектирования — Singleton в PHP. После написания статьи я искал новый вариант реализации паттерна: есть ли способ создать Singleton в PHP, не давая возможности создавать новые экзепляры класса с помощью Closure::bind()?


    How to fix Singleton in PHP


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


    Вот, собственно, код и ссылка на песочницу. Давайте его разберём:


    <?php
    
    final class Singleton
    {
        public static function getInstance()
        {
            static $instance;
    
            if (null === $instance) {
                $instance = new self();
            }
    
            return $instance;
        }
    
        private function __construct()
        {
            static $hasInstance = false;
    
            if ($hasInstance) {
                \trigger_error('Class is already instantiated', \E_USER_ERROR);
            }
    
            $hasInstance = true;
        }
    
        private function __clone()
        {
            \trigger_error('Class could not be cloned', \E_USER_ERROR);
        }
    
        private function __wakeup()
        {
            \trigger_error('Class could not be deserialized', \E_USER_ERROR);
        }
    }
    
    $s1 = Singleton::getInstance();
    \var_dump(\spl_object_id($s1));
    
    $createNewInstance = function () {
        return new self();
    };
    $newInstanceClosure = Closure::bind($createNewInstance, $s1, Singleton::class);
    
    // Fatal error:  Class is already instantiated
    $newInstanceClosure();

    статическую переменную $instance мы переносим в метод getInstance(), чтобы не иметь возможности получить к ней доступ с помощью операторов self и static в анонимной функции.


    В конструкторе класса так же добавляем статическую переменную, которая хранит булево-значение. При создании нового объекта мы проверяем значение этой переменной: если там хранится false — мы устанавливаем этой переменной значение true и объект успешно создаётся. При попытке создания нового объекта, код попадёт в if, так как при создании первого объекта мы записали значение true в статическую переменную $hasInstance, затем в теле if'а мы вызовем пользовательскую ошибку с текстом Class is already instantiated.


    В магических методах __clone() и __wakeup() мы так же вызываем пользовательские ошибки с соответствующими сообщениями для того, чтобы не иметь возможности создать объекты с помощью оператора clone и механизма сериализации в анонимной функции.


    При желании можно бросать исключения вместо пользовательских ошибок.


    Таким образом возможно создать всего один объект Singleton класса. Пока что я не нашёл способа сломать данную реализацию паттерна, поэтому если у кого-то получится это сделать — напишите об этом в комментарии :)


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

    Share post

    Comments 6

      +2
      Это хорошо подходит для наказания тех кто пишет тест который невозможно тестировать.
      Закоммитил класс для создания объекта которого в тестовой среде нужно полдня танцевать и петь, тут же садишься писать тесты для класса из поста выше.
        0
        Может быть, когда-нибудь в PHP допилят рефлексию и для static-переменных (сейчас их можно только читать, но не изменять).
          0
          Я думаю лучше писать код который можно просто тестировать без боли.
          +2

          Создать можно и больше с помощью ReflectionClass::newInstanceWithoutConstructor

            0
            Да, рефлексия непобедима :)
            0
            лучше вынести в трейт и добавить поддержку аргументов

            Only users with full accounts can post comments. Log in, please.