在PHP5中创建单例设计模式

213

如何使用PHP5类来创建单例类?


13
谁需要在PHP中使用单例模式? - Gordon
1
@Andrew 不要再实例化一个连接到数据库的第二个实例了。将该实例传递到需要它的位置。使用Singleton是一种代码坏味道。更多信息请参见http://gooh.posterous.com/singletons-in-php。 - Gordon
1
@Andrew 不,它不会。如果你需要限制实例为一个,请不要实例化第二个实例。单例模式不会减少应用程序的复杂性,相反会增加它。它是全局的,并引入耦合,从而降低可维护性并增加单元测试的复杂性。这是事实,而非看法。另外,你不需要确保开发人员不能实例化第二个实例。不要过于照顾他们。还有,接口是不同的东西。欢迎来聊天室与我们讨论。 - Gordon
3
嗯,没有冒犯的意思,但我建议在我们继续讨论之前,您先阅读一本关于软件质量的书。单例模式并不能简化正常的维护和开发工作,反而会使其变得更加复杂。事实上,恰恰相反:单元测试才是首要的简化和促进开发的方式。 - Gordon
3
@Andrew: 你现在假设只需要一个数据库连接。当你的需求改变,实际上需要连接两个数据库服务器时会发生什么?更不用说如果你不能信任你的团队做正确的事情,创建单例模式也无济于事。从一开始就做好事情,并获得一个可以信任的团队,你就会没问题。 - ircmaxell
4
单例模式被过度使用并不意味着它是一个应该避免使用的糟糕模式。不要抵制单例模式。有时,它是解决某些问题的完美解决方案。最好开始阐述为什么我们不应该使用它,而不是试图情绪化地贬低它。 - Gilles Lesire
22个回答

271
/**
 * Singleton class
 *
 */
final class UserFactory
{
    private static $inst = null;

    // Prevent cloning and de-serializing
    private function __clone(){}
    private function __wakeup(){}


    /**
     * Call this method to get singleton
     *
     * @return UserFactory
     */
    public static function Instance()
    {
        if ($inst === null) {
            $inst = new UserFactory();
        }
        return $inst;
    }
    
    /**
     * Private ctor so nobody else can instantiate it
     *
     */
    private function __construct()
    {
        
    }
}

使用方法:

$fact = UserFactory::Instance();
$fact2 = UserFactory::Instance();

$fact == $fact2;

但是:

$fact = new UserFactory()

抛出一个错误。

请参阅http://php.net/manual/en/language.variables.scope.php#language.variables.scope.static 了解静态变量作用域以及为什么设置static $inst = null;有效。


60
要比较这两个实例,你应该使用 === 而不是 ==。如果 $fact1 和 $fact2 都是相同类的对象,== 会返回 true,但是只有当它们都是同一个对象的相同实例时,=== 才会返回 true。 - Keith Twombley
12
克隆方法应该也是私有的。 - Alex Petrov
25
每次调用Instance()方法,这种方法会将UserFactory实例重置为null吗?在Java中,$inst变量应该是一个私有静态属性,不应该反复重置,否则你也可以不把它设为单例。 - Rudy Garcia
8
这是一篇很好的文章,解释了为什么在函数中将变量声明为静态变量可以按照作者的意图工作的原因和方法:http://php.net/manual/en/language.variables.scope.php#language.variables.scope.static - hereswhatidid
10
以后任何人遇到这种情况时,你应该使用$inst = new self();而不是$inst = new UserFactory();。为使用 PHP 内置的方法给你加 1 分。 - akahunahi
显示剩余6条评论

123

很不幸,Inwdr的回答在存在多个子类时会出现问题。

这里提供了一个正确可继承的单例基类。

class Singleton
{
    private static $instances = array();
    protected function __construct() {}
    protected function __clone() {}
    public function __wakeup()
    {
        throw new Exception("Cannot unserialize singleton");
    }

    public static function getInstance()
    {
        $cls = get_called_class(); // late-static-bound class name
        if (!isset(self::$instances[$cls])) {
            self::$instances[$cls] = new static;
        }
        return self::$instances[$cls];
    }
}

测试代码:

class Foo extends Singleton {}
class Bar extends Singleton {}

echo get_class(Foo::getInstance()) . "\n";
echo get_class(Bar::getInstance()) . "\n";

1
这是目前最接近正确的单例模式实现。您还应该考虑添加__wakeup()方法以防止反序列化。 - Robert Rossmann
实际上,您必须手动抛出异常或引发错误 - 将函数声明为受保护/私有只会引发 E_WARNING,表示无法访问该方法,但否则将继续执行。 - Robert Rossmann
谢谢。通常我会将所有警告等转换为异常,所以当我测试时忘记了这个区别 :P - mpartel
这是我找到的唯一一个能够正确处理多个子类的解决方案。谢谢! - Bob Dankert

120

PHP 5.3 允许通过延迟静态绑定创建可继承的单例类:

class Singleton
{
    protected static $instance = null;

    protected function __construct()
    {
        //Thou shalt not construct that which is unconstructable!
    }

    protected function __clone()
    {
        //Me not like clones! Me smash clones!
    }

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

这解决了一个问题,即在PHP 5.3之前,任何继承Singleton的类都会产生其父类的实例而不是自己的实例。

现在你可以这样做:

class Foobar extends Singleton {};
$foo = Foobar::getInstance();

现在$foo将成为Foobar的一个实例,而不是Singleton的实例。


4
"子类应该拥有自己的静态变量。请检查以下代码:echo get_class(Foobar :: getInstance()); echo get_class(Singleton :: getInstance());" - Brock Adams
4
完全不起作用,只是碰巧Foobar是您构建的第一个类? - Chris KL
15
当存在多个子类时,这种方法无法正常工作!$instance 存在于 Singleton 中,而非子类。在某些子类被实例化后,getInstance() 会返回该实例,对于所有的子类都是一样的。 - mpartel
1
我已经尝试为您编辑此内容,我们将看到它是否会显示出来 ;) 基本上,上面的答案只需要提供一个定义好的“__sleep()”函数来抛出错误。这解决了Adrian复制实例的问题。它还需要使用“get_called_class()”来解决多个子类级别的问题。 - niaccurshi
1
@clive,我并不是在暗示你删除了我的编辑有错,只是想解释一下情况 :) - niaccurshi
显示剩余4条评论

39

现代和真实的单例模式实现方式是:

<?php

/**
 * Singleton Pattern.
 * 
 * Modern implementation.
 */
class Singleton
{
    /**
     * Call this method to get singleton
     */
    public static function instance()
    {
      static $instance = false;
      if( $instance === false )
      {
        // Late static binding (PHP 5.3+)
        $instance = new static();
      }

      return $instance;
    }

    /**
     * Make constructor private, so nobody can call "new Class".
     */
    private function __construct() {}

    /**
     * Make clone magic method private, so nobody can clone instance.
     */
    private function __clone() {}

    /**
     * Make sleep magic method private, so nobody can serialize instance.
     */
    private function __sleep() {}

    /**
     * Make wakeup magic method private, so nobody can unserialize instance.
     */
    private function __wakeup() {}

}

现在你可以像这样使用它。

<?php

/**
 * Database.
 *
 * Inherited from Singleton, so it's now got singleton behavior.
 */
class Database extends Singleton {

  protected $label;

  /**
   * Example of that singleton is working correctly.
   */
  public function setLabel($label)
  {
    $this->label = $label;
  }

  public function getLabel()
  {
    return $this->label;
  }

}

// create first instance
$database = Database::instance();
$database->setLabel('Abraham');
echo $database->getLabel() . PHP_EOL;

// now try to create other instance as well
$other_db = Database::instance();
echo $other_db->getLabel() . PHP_EOL; // Abraham

$other_db->setLabel('Priler');
echo $database->getLabel() . PHP_EOL; // Priler
echo $other_db->getLabel() . PHP_EOL; // Priler

正如您所看到的,这种实现方式要更加灵活。


4
这是这个主题中关于Singleton模式最清晰的答案。谢谢。 - Gus
instance函数中,$instance应该是null而不是false - KKK
是的,但它不是函数,而是方法。 - Abraham Tugalov
1
现在至少必须将__wakeup()__sleep()声明为公共方法。当然,在这些方法中你可以抛出异常。 - cgogolin
为什么基类不是“抽象”的? - Johnson_145
显示剩余2条评论

26

你可能需要添加一个私有的__clone()方法来禁止实例的克隆。

private function __clone() {}
如果您不包含此方法,则可能发生以下情况。
$inst1=UserFactory::Instance(); // to stick with the example provided above
$inst2=clone $inst1;

现在 $inst1 !== $inst2 - 它们不再是同一个实例了。


12
<?php
/**
 * Singleton patter in php
 **/
trait SingletonTrait {
   protected static $inst = null;

  /**
   * call this method to get instance
   **/
   public static function getInstance(){
      if (static::$inst === null){
         static::$inst = new static();
      }
      return static::$inst;
  }

  /**
   * protected to prevent clonning 
   **/
  protected function __clone(){
  }

  /**
   * protected so no one else can instance it 
   **/
  protected function __construct(){
  }
}

使用方法:

/**
 *  example of class definitions using SingletonTrait
 */
class DBFactory {
  /**
   * we are adding the trait here 
   **/
   use SingletonTrait;

  /**
   * This class will have a single db connection as an example
   **/
  protected $db;


 /**
  * as an example we will create a PDO connection
  **/
  protected function __construct(){
    $this->db = 
        new PDO('mysql:dbname=foodb;port=3305;host=127.0.0.1','foouser','foopass');
  }
}
class DBFactoryChild extends DBFactory {
  /**
   * we repeating the inst so that it will differentiate it
   * from UserFactory singleton
   **/
   protected static $inst = null;
}


/**
 * example of instanciating the classes
 */
$uf0 = DBFactoryChild::getInstance();
var_dump($uf0);
$uf1 = DBFactory::getInstance();
var_dump($uf1);
echo $uf0 === $uf1;

回应:

object(DBFactoryChild)#1 (0) {
}
object(DBFactory)#2 (0) {
}

如果您使用的是PHP 5.4: trait 是一种选择,因此您不必浪费继承层次结构就可以拥有Singleton模式

同时请注意,无论您使用traits还是extends Singleton类, 都存在一个松散的端口需要创建子类的单例,如果您不添加以下代码行:

   protected static $inst = null;

在子类中

出现意外的结果将是:

object(DBFactoryChild)#1 (0) {
}
object(DBFactoryChild)#1 (0) {
}

10
protected  static $_instance;

public static function getInstance()
{
    if(is_null(self::$_instance))
    {
        self::$_instance = new self();
    }
    return self::$_instance;
}

这段代码可以适用于任何类,而不需要关心它的类名。


8

支持多个对象,每个类只需一行代码:

此方法将强制任何您希望的类成为单例模式,您只需要向要创建单例模式的类添加1个方法即可完成。

此外,此方法还将对象存储在“SingleTonBase”类中,因此您可以通过递归“SingleTonBase”对象来调试系统中使用的所有对象。


创建一个名为SingletonBase.php的文件,并将其包含在脚本的根目录中!

代码如下:

abstract class SingletonBase
{
    private static $storage = array();

    public static function Singleton($class)
    {
        if(in_array($class,self::$storage))
        {
            return self::$storage[$class];
        }
        return self::$storage[$class] = new $class();
    }
    public static function storage()
    {
       return self::$storage;
    }
}

然后,对于任何你想要创建单例的类,只需添加这个简短的单例方法。

public static function Singleton()
{
    return SingletonBase::Singleton(get_class());
}

这里有一个小例子:

include 'libraries/SingletonBase.resource.php';

class Database
{
    //Add that singleton function.
    public static function Singleton()
    {
        return SingletonBase::Singleton(get_class());
    }

    public function run()
    {
        echo 'running...';
    }
}

$Database = Database::Singleton();

$Database->run();

您可以在任何类中添加此单例函数,它将仅为每个类创建1个实例。

注意:您应始终将__construct设置为私有,以消除new Class(); 实例化的使用。


5
class Database{

        //variable to hold db connection
        private $db;
        //note we used static variable,beacuse an instance cannot be used to refer this
        public static $instance;

        //note constructor is private so that classcannot be instantiated
        private function __construct(){
          //code connect to database  

         }     

         //to prevent loop hole in PHP so that the class cannot be cloned
        private function __clone() {}

        //used static function so that, this can be called from other classes
        public static function getInstance(){

            if( !(self::$instance instanceof self) ){
                self::$instance = new self();           
            }
             return self::$instance;
        }


        public function query($sql){
            //code to run the query
        }

    }


Access the method getInstance using
$db = Singleton::getInstance();
$db->query();

5
您真的不需要使用单例模式,因为它被认为是一种反模式。基本上,完全没有实现此模式的理由。首先阅读此文:PHP单例类的最佳实践
如果您仍然认为需要使用单例模式,则可以编写一个类来扩展我们的SingletonClassVendor抽象类,从而使我们能够获得单例功能。
这是我想出的解决问题的方法。
<?php
namespace wl;


/**
 * @author DevWL
 * @dosc allows only one instance for each extending class.
 * it acts a litle bit as registry from the SingletonClassVendor abstract class point of view
 * but it provides a valid singleton behaviour for its children classes
 * Be aware, the singleton pattern is consider to be an anti-pattern
 * mostly because it can be hard to debug and it comes with some limitations.
 * In most cases you do not need to use singleton pattern
 * so take a longer moment to think about it before you use it.
 */
abstract class SingletonClassVendor
{
    /**
     *  holds an single instance of the child class
     *
     *  @var array of objects
     */
    protected static $instance = [];

    /**
     *  @desc provides a single slot to hold an instance interchanble between all child classes.
     *  @return object
     */
    public static final function getInstance(){
        $class = get_called_class(); // or get_class(new static());
        if(!isset(self::$instance[$class]) || !self::$instance[$class] instanceof $class){
            self::$instance[$class] = new static(); // create and instance of child class which extends Singleton super class
            echo "new ". $class . PHP_EOL; // remove this line after testing
            return  self::$instance[$class]; // remove this line after testing
        }
        echo "old ". $class . PHP_EOL; // remove this line after testing
        return static::$instance[$class];
    }

    /**
     * Make constructor abstract to force protected implementation of the __constructor() method, so that nobody can call directly "new Class()".
     */
    abstract protected function __construct();

    /**
     * Make clone magic method private, so nobody can clone instance.
     */
    private function __clone() {}

    /**
     * Make sleep magic method private, so nobody can serialize instance.
     */
    private function __sleep() {}

    /**
     * Make wakeup magic method private, so nobody can unserialize instance.
     */
    private function __wakeup() {}

}

使用示例:

/**
 * EXAMPLE
 */

/**
 *  @example 1 - Database class by extending SingletonClassVendor abstract class becomes fully functional singleton
 *  __constructor must be set to protected becaouse: 
 *   1 to allow instansiation from parent class 
 *   2 to prevent direct instanciation of object with "new" keword.
 *   3 to meet requierments of SingletonClassVendor abstract class
 */
class Database extends SingletonClassVendor
{
    public $type = "SomeClass";
    protected function __construct(){
        echo "DDDDDDDDD". PHP_EOL; // remove this line after testing
    }
}


/**
 *  @example 2 - Config ...
 */
class Config extends SingletonClassVendor
{
    public $name = "Config";
    protected function __construct(){
        echo "CCCCCCCCCC" . PHP_EOL; // remove this line after testing
    }
}

为了证明它按预期工作:

/**
 *  TESTING
 */
$bd1 = Database::getInstance(); // new
$bd2 = Database::getInstance(); // old
$bd3 = Config::getInstance(); // new
$bd4 = Config::getInstance(); // old
$bd5 = Config::getInstance(); // old
$bd6 = Database::getInstance(); // old
$bd7 = Database::getInstance(); // old
$bd8 = Config::getInstance(); // old

echo PHP_EOL."COMPARE ALL DATABASE INSTANCES".PHP_EOL;
var_dump($bd1);
echo '$bd1 === $bd2' . ($bd1 === $bd2)? ' TRUE' . PHP_EOL: ' FALSE' . PHP_EOL; // TRUE
echo '$bd2 === $bd6' . ($bd2 === $bd6)? ' TRUE' . PHP_EOL: ' FALSE' . PHP_EOL; // TRUE
echo '$bd6 === $bd7' . ($bd6 === $bd7)? ' TRUE' . PHP_EOL: ' FALSE' . PHP_EOL; // TRUE

echo PHP_EOL;

echo PHP_EOL."COMPARE ALL CONFIG INSTANCES". PHP_EOL;
var_dump($bd3);
echo '$bd3 === $bd4' . ($bd3 === $bd4)? ' TRUE' . PHP_EOL: ' FALSE' . PHP_EOL; // TRUE
echo '$bd4 === $bd5' . ($bd4 === $bd5)? ' TRUE' . PHP_EOL: ' FALSE' . PHP_EOL; // TRUE
echo '$bd5 === $bd8' . ($bd5 === $bd8)? ' TRUE' . PHP_EOL: ' FALSE' . PHP_EOL; // TRUE

在阅读更多得票最高的答案时,我脑海中有类似的想法。幸运的是它已经在这里了 :) - hatef

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