Расширение синглетонов в PHP

Я работаю в среде веб-приложений, и часть ее состоит из ряда сервисов, реализованных как синглтоны. Все они расширяют класс Service, в котором реализовано поведение singleton, выглядящее примерно так:

class Service {
    protected static $instance;

    public function Service() {
        if (isset(self::$instance)) {
            throw new Exception('Please use Service::getInstance.');
        }
    }

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

Теперь, если у меня есть класс FileService, реализованный следующим образом:

class FileService extends Service {
    // Lots of neat stuff in here
}

... вызов FileService::getInstance() не приведет к экземпляру FileService, как я этого хочу, а к экземпляру Service. Я предполагаю, что проблема здесь заключается в ключевом слове «self», используемом в конструкторе службы.

Есть ли другой способ добиться того, чего я хочу здесь? Одноэлементный код состоит всего из нескольких строк, но я все же хотел бы избегать избыточности кода, когда это возможно.


person Johan Fredrik Varen    schedule 27.06.2010    source источник


Ответы (6)


Код:

abstract class Singleton
{
    protected function __construct()
    {
    }

    final public static function getInstance()
    {
        static $instances = array();

        $calledClass = get_called_class();

        if (!isset($instances[$calledClass]))
        {
            $instances[$calledClass] = new $calledClass();
        }

        return $instances[$calledClass];
    }

    final private function __clone()
    {
    }
}

class FileService extends Singleton
{
    // Lots of neat stuff in here
}

$fs = FileService::getInstance();

Если вы используете PHP ‹ 5.3, добавьте и это:

// get_called_class() is only in PHP >= 5.3.
if (!function_exists('get_called_class'))
{
    function get_called_class()
    {
        $bt = debug_backtrace();
        $l = 0;
        do
        {
            $l++;
            $lines = file($bt[$l]['file']);
            $callerLine = $lines[$bt[$l]['line']-1];
            preg_match('/([a-zA-Z0-9\_]+)::'.$bt[$l]['function'].'/', $callerLine, $matches);
        } while ($matches[1] === 'parent' && $matches[1]);

        return $matches[1];
    }
}
person Amy B    schedule 27.06.2010
comment
К вашему сведению: этот код использует get_called_class, добавленный в PHP. 5.3. Сделать это в более ранних версиях немного сложнее. - person Charles; 27.06.2010
comment
Святые yikes, это страшно. Представьте себе вызов getInstance дюжину раз, это дюжина открытий и дюжина чтений файла класса. - person Charles; 27.06.2010
comment
Вот почему люди должны обновиться до последней и лучшей версии ^^ - person Amy B; 27.06.2010
comment
Спасибо! На самом деле я смотрел на эту функцию сразу после того, как опубликовал вопрос, но не знал, как ее использовать для решения проблемы. Теперь мне просто нужно подождать, пока ребята из веб-отеля обновятся до PHP5.3 :) - person Johan Fredrik Varen; 27.06.2010
comment
@Johan, возможно, вы захотите вообще отказаться от синглтонов. Они создают жесткую связь с вашим приложением и их трудно тестировать. Вы можете решить проблему «может иметь только один экземпляр» с помощью Dependency Injection Framework или реестра. - person Gordon; 27.06.2010
comment
Должен ли тогда конструктор расширяющего класса быть общедоступным? - person wiktus239; 21.03.2015
comment
Я поместил частный метод getCalledClass() в класс Singleton, где я проверяю, существует ли функция get_called_class (я хочу собрать весь код вместе). Я делаю неправильно? Кажется, работает :) - person Briganti; 29.06.2015
comment
Это не работает для меня. Похоже, что статические $instances инициализируются новым и пустым массивом каждый раз, когда вызывается getInstance - person Philip; 11.12.2020

Если бы я уделял больше внимания в классе 5.3, я бы сам знал, как решить эту проблему. Используя новую функцию позднего статического связывания PHP 5.3, я считаю, что предложение Coronatus можно упростить до следующего:

class Singleton {
    protected static $instance;

    protected function __construct() { }

    final public static function getInstance() {
        if (!isset(static::$instance)) {
            static::$instance = new static();
        }

        return static::$instance;
    }

    final private function __clone() { }
}

Я попробовал это, и это работает как шарм. Однако Pre 5.3 — это совсем другая история.

person Johan Fredrik Varen    schedule 29.06.2010
comment
Кажется, что есть только одно поле $instance для всех подклассов, поэтому возвращается только синглтон класса, где getInstance() вызывается первым. - person C-Otto; 21.11.2013
comment
Это наивный подход, который, к сожалению, вообще не работает, как уже упоминал C-Otto. Проголосовали против: Не делайте этого дома ;-) - person Phil; 10.03.2015
comment
Как они сказали выше... это неправильно, это даст вам экземпляр первого класса, который использовал getInstance() - person Juan; 02.06.2015

Это фиксированный ответ Йохана. PHP 5.3+

abstract class Singleton
{
    protected function __construct() {}
    final protected function __clone() {}

    final public static function getInstance()
    {
        static $instance = null;

        if (null === $instance)
        {
            $instance = new static();
        }

        return $instance;
    }
}
person Alexey Filatov    schedule 21.11.2017

Я нашел хорошее решение.

следующий мой код

abstract class Singleton
{
    protected static $instance; // must be protected static property ,since we must use static::$instance, private property will be error

    private function __construct(){} //must be private !!! [very important],otherwise we can create new father instance in it's Child class 

    final protected function __clone(){} #restrict clone

    public static function getInstance()
    {
        #must use static::$instance ,can not use self::$instance,self::$instance will always be Father's static property 
        if (! static::$instance instanceof static) {
            static::$instance = new static();
        }
        return static::$instance;
    }
}

class A extends Singleton
{
   protected static $instance; #must redefined property
}

class B extends A
{
    protected static $instance;
}

$a = A::getInstance();
$b = B::getInstance();
$c = B::getInstance();
$d = A::getInstance();
$e = A::getInstance();
echo "-------";

var_dump($a,$b,$c,$d,$e);

#object(A)#1 (0) { }
#object(B)#2 (0) { } 
#object(B)#2 (0) { } 
#object(A)#1 (0) { } 
#object(A)#1 (0) { }

Вы можете сослаться на http://php.net/manual/en/language.oop5.late-static-bindings.php для получения дополнительной информации.

person Martin Ding    schedule 19.06.2018
comment
с какой версии это поддерживается? - person Maxwell s.c; 08.12.2018

Использование типажа вместо абстрактного класса позволяет расширить одноэлементный класс.

Используйте трейт SingletonBase для родительского одноэлементного класса.

Используйте трейт SingletonChild для его одноэлементных дочерних элементов.

interface Singleton
{

    public static function getInstance(): Singleton;

}

trait SingletonBase
{

    private static $instance=null;

    abstract protected function __construct();

    public static function getInstance(): Singleton {

       if (is_null(self::$instance)) {

          self::$instance=new static();

       }

       return self::$instance;

    } 

    protected function clearInstance(): void {

        self::$instance=null;

    }

    public function __clone()/*: void*/ {

        trigger_error('Class singleton '.get_class($this).' cant be cloned.');
    }

    public function __wakeup(): void {

        trigger_error('Classe singleton '.get_class($this).' cant be serialized.');

    }

}

trait SingletonChild
{

    use SingletonBase;

}

class Bar
{

    protected function __construct(){

    }

}

class Foo extends Bar implements Singleton
{

      use SingletonBase;

}

class FooChild extends Foo implements Singleton
{

      use SingletonChild; // necessary! If not, the unique instance of FooChild will be the same as the unique instance of its parent Foo

}
person Viney    schedule 07.04.2020
comment
Если вы расширите этот синглтон двумя разными объектами, он переопределит экземпляр self::$ с последним дочерним элементом SingletonBase. - person Stanislav Stankov; 06.12.2020

    private static $_instances = [];

    /**
     * gets the instance via lazy initialization (created on first usage).
     */
    public static function getInstance():self
    {
        $calledClass = class_basename(static::class);

        if (isset(self::$_instances[$calledClass])) {
            self::$_instances[$calledClass] = new static();
        }

        return self::$_instances[$calledClass];
    }

Единственная проблема с этим, если у вас есть одно и то же имя Singletons.

person Stanislav Stankov    schedule 06.12.2020