在PHP中扩展单例模式

29

我正在开发一个web应用程序框架,其中一部分包括多个服务,所有服务都实现为单例。它们都扩展自一个Service类,在这个类中实现了单例模式的行为,看起来类似于以下代码:

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实例。我猜问题出在Service构造函数中使用的"self"关键字。

有没有其他方法可以实现我想要的效果?虽然单例模式只需要写几行代码,但我仍然希望尽可能避免任何冗余代码。

7个回答

61

代码:

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];
    }
}

1
天哪,这太可怕了。 想象一下调用“getInstance”十几次,这就是打开和读取类文件的十几次。 - Charles
1
这就是为什么人们应该升级到最新和最好的版本。^^ - Amy B
那么扩展类的构造函数是否必须是公共的呢? - wiktus239
我在Singleton类中放置了一个私有的getCalledClass()方法,在其中检查get_called_class函数是否存在(我想把所有代码放在一起)。我做错了吗?它似乎可以工作 :) - BCsongor
这对我不起作用。看起来静态变量$instances在每次调用getInstance时都被初始化为一个新的空数组。 - Philip
显示剩余4条评论

9

如果我在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() { }
}

我尝试了一下,它非常好用。然而,在5.3之前还是完全不同的情况。


7
似乎所有的子类都只有一个名为 $instance 的字段,因此只会返回调用 getInstance() 方法的类的单例。 - C-Otto
这是一种天真的方法,不幸的是像C-Otto已经提到的那样根本行不通。被投票否决:不要在家尝试这个;-) - Phil
就像他们上面所说的一样..是错误的,这将会给你使用getInstance()方法的第一个类的实例。 - chesscov77

3
我找到了一个很好的解决方案。
以下是我的代码。
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/zh/language.oop5.late-static-bindings.php了解更多信息。

这个支持从哪个版本开始? - Maxwell s.c
在这种情况下,Singleton::$instance 仍然在所有类型之间共享,所以每次调用不同的类型时都会替换它。在你的示例中,$d !== $a,即 $d$a 不是同一个对象。 - Elte Hupkes
@ElteHupkes 你找到合适的解决方案了吗? - undefined
@YusufBayrak 我其实是错的(我打算删除我的评论),如果你不忘记重新定义protected static $instance,这个答案似乎是正确的。我不喜欢这个要求(干脆在那个点上使用一个trait),所以我个人使用一个数组来跟踪由完全指定的类名实例化的对象。虽然有点丑陋,但至少它是一劳永逸的 - 只有一次丑陋的体验 ;)。 - undefined

2

这是修复了Johan的答案。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;
    }
}

这与Johan的答案存在相同的问题,至少在PHP 8.1中如此。$instance在整个继承树中是共享的。 - Elte Hupkes

1

我遇到了这个问题,因为我正在使用单例类来管理类似缓存的对象并希望进行扩展。

Amy B的答案对我来说有点过于复杂,所以我进一步深入挖掘,最终找到了以下解决方案,运行良好:

abstract class Singleton
{
    protected static $instance = null;

    protected function __construct()
    {
    }

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

    final private function __clone()
    {
    }
}

class FileService extends Singleton
{
  protected static $instance = null;
}
    
$fs = FileService::getInstance();

只需覆盖$instance类属性即可解决该问题。 仅在PHP 8上进行了测试,但我猜测这对旧版本也有效。


0

使用trait而不是抽象类可以扩展单例类。

将trait SingletonBase用作父单例类。

将trait 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

}

如果您使用两个不同的对象扩展了该单例,它将使用SingletonBase的最新子类覆盖self::$instance。 - Stanislav Stankov

0
    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];
    }

这个的唯一问题是,如果你有相同名称的单例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接