Защитите порождающий паттерн Singleton от размножения через clone и unserialize
Реализуйте синглтон Config, получить который можно только через Config::getInstance() и который нельзя размножить никаким другим путём.
Требования: new Config() снаружи невозможен; единственный экземпляр хранится на классе и возвращается каждым вызовом getInstance(); clone $instance обязан падать; и unserialize(serialize($instance)) тоже не должен давать второй объект.
<?php
final class Config
{
private static ?self $instance = null;
private array $values = [];
// ваш код здесь — конструктор, getInstance() и две защиты
public function get(string $key): mixed
{
return $this->values[$key] ?? null;
}
}
Допишите реализацию.
Сделайте __construct приватным, чтобы закрыть new, держите экземпляр в private static-слоте и выдавайте его из getInstance(). Затем закройте два чёрных хода: приватный __clone() делает clone фатальным, а __wakeup() обязан бросать: unserialize() собирает объект без конструктора.
- ✗Закрыть
newприватным конструктором, оставивclone $instanceполностью открытым - ✗Забыть, что
unserialize()собирает объект, вообще не вызывая__construct - ✗Считать, что клонирование запрещает именно
finalна классе
- →Почему
unserialize()обходит приватный конструктор и что с этим делает__wakeup()? - →Почему синглтон трудно тестировать и что бы вы внедряли вместо него?
Синглтон закрывают тремя дверями, а не одной. new закрывает приватный конструктор, clone — приватный __clone, а unserialize() — __wakeup(), который обязан бросать исключение: десериализация создаёт объект в обход конструктора.
<?php
final class Config
{
private static ?self $instance = null;
private array $values = [];
private function __construct()
{
$this->values = ['env' => 'prod'];
}
public static function getInstance(): self
{
return self::$instance ??= new self();
}
private function __clone() {}
public function __wakeup(): void
{
throw new \LogicException('Config is a singleton and cannot be unserialized.');
}
public function get(string $key): mixed
{
return $this->values[$key] ?? null;
}
}
__wakeup() объявлен public намеренно: сигнатура магического метода должна совпадать с родительской, а спрятать его нельзя — поэтому защита в том, что он бросает, а не в его видимости. __clone же достаточно сделать private: clone $config снаружи станет фатальной ошибкой.