Singleton pattern in PHP

Here’s a small tutorial and example PHP code on how to maintain a single instance of one object (Singleton pattern) throughout the script. You might have a complex script with bunch of objects flying around and it’s hard to keep them all available in global context. Here’s a simple OOP solution to a single object instance in PHP5.


The idea is, that if you have several classes for your application like MyUser, MyGroup, MySomething. All information about users, groups, etc is stored in database and loaded when you need it. So now, you might have to do something like this:

$User = new MyUser;
$User->load(2); // load user with Id 2 from database
// or even
$User = new MyUser(2); // which loads the user directly as constructor is called
// within that load() method, you also have to load the MyGroup object with Id 1
// so you can later do that:
$Group = $User->getGroup(); // return MyGroup: an object representing a group user belongs to
// etc.

So now, if you load 5 users from database, that are in the same “MyGroup”, you’ll have 5 different (but same group Id) MyGroup object instances flying around, as PHP will not handle object instances for you. So, a new proposed method of loading such objects would be:

$User = MyUser::getInstance(2);
// so now, MyUser class will use this, to get the MyGroup loaded:
// $Group = MyGroup::getInstance(1);
// so when you do this:
$Group = $User->getGroup();
// on as many users objects as you want, you'll always receive the *same* object or same object instance.

To achieve this functionality you only need an unique Id of the object (eg:. unique Id of the user or group). To start the implementation, we’ll create an abstract class definition that all classes, using this functionality, should extend:

<?php
/**
 * PHP5 Object instance control
 *
 */
abstract class BInstanceControl {

  /**
   * Array of active instances
   * @var array
   */
  static public $_Instances	= array();

  /**
   * Object Id
   * @param mixed
   */
  protected $id			= null;

  /**
   * Add instance
   *
   * @param object $o Any object with getId method
   */
  static public function addInstance($o){
    if (!is_object($o))
      throw new Exception('Provided argument is not an object');
    $className = get_class($o);
    if ($className === false)
      throw new Exception('Could not get class name from provided object');
    if (!isset(self::$_Instances[$className])){
      self::$_Instances[$className] = array();
    }
    if (!isset(self::$_Instances[$className][$o->getId()])){
      self::$_Instances[$className][$o->getId()] = $o;
    }
    else {
      throw new Exception('Object instance of class: ' . $className .
						  ' with Id: ' . $o->getId() .
						  ' already exists');
    }
  }

  /**
   * Get instance method has to be provided by the class
   *
   * Ussualy it looks like this (just a call to _getInstance)
   * static public function getInstance($id){
   *   return self::_getInstance(__CLASS__, $id);
   * }
   *
   * @param $mixed $id Object Id
   * @return object
   */
  abstract static public function getInstance($id);
  /**
   * Get instance
   *
   * If class instance does not exists, it will try to load a new one. Class has to have
   * a method load($id) which loads it self.
   *
   * @param string $className Class name
   * @param int $id Id
   * @return object
   */
  static public function _getInstance($className, $id){
    if (isset(self::$_Instances[$className][$id])){
      return self::$_Instances[$className][$id];
    }
    else {
      if (!isset(self::$_Instances[$className]))
        self::$_Instances[$className] = array();
      $o = new $className;
      $o->load($id);
      return $o;
    }
  }

  /**
   * Remove instance
   *
   * @param object $o
   */
  static public function removeInstance($o){
    if (!is_object($o))
      throw new Exception('Provided argument is not an object');
    $className = get_class($o);
    if ($className === false)
      throw new Exception('Could not get class name from provided object');
    $id = $o->getId();
    if (isset(self::$_Instances[$className][$id])){
      unset(self::$_Instances[$className][$id]);
      unset($o);
    }
  }

  /**
   * Remove all instances of class name
   *
   * This method will remove all instances of provided class name.
   * If class has an unload method, it will call that too!
   *
   * @param string $className Class Name
   */
  static public function removeAllInstances($className){
    if (isset(self::$_Instances[$className])){
      foreach(self::$_Instances[$className] as $Instance){
        if (method_exists($Instance, 'unload'))
          $Instance->unload();
      }
      unset(self::$_Instances[$className]);
    }
  }

  /**
   * Get all instances of one class
   *
   * @param string $className
   * @return array
   */
  static public function getAllInstances($className){
    if (isset(self::$_Instances[$className])){
      return self::$_Instances[$className];
    }
    else {
      return array();
    }
  }

  /**
   * Check if class instance exists by Id
   *
   * @return bool
   */
  static public function exists($className, $id){
    return isset(self::$_Instances[$className][$id]);
  }

  /**
   * Default instance controlled object destructor
   *
   * If Id is set, it will remove class instance
   */
  public function __destruct(){
    if ($this->id !== null)
      self::removeInstance($this);
  }

  /**
   * Default instance controlled object sleep method
   *
   * It will store only Id
   */
  public function __sleep(){
    self::removeInstance($this);
    return array('id');
  }

  /**
   * Default instance controlled object wakeup method
   *
   * It will load the class again, using load method.
   */
  public function __wakeup(){
    $this->__construct();
    if ($this->getId() !== null){
      if (!isset(self::$_Instances[get_class($this)][$this->getId()])){
        $this->load($this->getId());
      }
      else {
        self::removeInstance($this);
        $this->load($this->getId());
      }
    }
  }

  /**
   * Set object Id
   *
   * This method will also automaticaly add instance of this class to instances list.
   * @param mixed $id Id
   */
  protected function setId($id){
    $this->id = $id;
    self::addInstance($this);
  }

  /**
   * Get object Id
   *
   * @return mixed
   */
  public function getId(){
    return $this->id;
  }

  /**
   * Load method
   *
   * Child classes should implement this.
   * @param mixed $id
   */
  abstract protected function load($id);
}
?>

The most basic usage of instance control with this class is:

// we need BInstanceControl class definition
require('BInstanceControl.class.php');
// Let's write our example MyUser class with Instance control
class MyUser extends BInstanceControl {
  // we don't need to define the Id field, as BInstanceControl
  // defines it and methods like getId() and setId()
  // users name
  private $name = null;

  // we need getInstance static method
  // we could use BInstanceControl::_getInstance directly, but it's easier to
  // define it in working class, for later ease of use.
  static public function getInstance($id){
    return self::_getInstance(__CLASS__, $id);
  }

  // We also need the load() method
  public function load($id){
    // load the user from DB
    $this->setName('test user');
    // DB also loads the Group with Id 1 of the user
    $this->setGroup(MyGroup::getInstance(1));
  }

  // you might also provide the unload method
  // that will be called, when object is destroyed and
  // instance is removed, or you want to force the unload.
  public function unload(){
    echo "User is being unloaded!n";
  }

  // some methods of user class
  public function setName($name){
    $this->name = $name;
  }

  public function getName(){
    return $this->name;
  }

  public function setGroup(MyGroup $Group){
    $this->Group = $Group;
  }

  public function getGroup(){
    return $this->Group;
  }
  // etc etc
}

// almost same code for MyGroup class
class MyGroup extends BInstanceControl {
  private $name = null;
  // another get instance method
  static public function getInstance($id){
    return self::_getInstance(__CLASS__, $id);
  }

  // load method for group
  public function load($id){
    // load group from database
    $this->setName('my group');
  }

  // some methods for MyGroup class
  public function setName($name){
    $this->name = $name;
  }

  public function getName(){
    return $this->name;
  }
}

So, for example, let’s say we have users with id 1,2,3 all in group with Id 1. Before instance control, if you did something like this:

$User1 = new MyUser(1);
$User2 = new MyUser(2);
$User3 = new MyUser(3);
// let's say all those users were loaded from database with it's group object with Id 1
// so something like this should change the groups name:
$User1->getGroup()->setName('My new group name');
// now, if we check all User objects:
echo "User1: " . $User1->getGroup()->getName() . "n";
echo "User2: " . $User2->getGroup()->getName() . "n";
echo "User3: " . $User3->getGroup()->getName() . "n";

The output here would be something like this:

User1: My new group name
User2: my group
User3: my group

Here we did change the group name, but only on object instance that is stored within User1 object, not on all other MyUser objects (even tough the group_id is the same). In instance controlled object, you’d see something like this:

$User1 = MyUser::getInstance(1);
$User2 = MyUser::getInstance(2);
$User3 = MyUser::getInstance(3);
$User1->getGroup()->setName('My new group name');
// now, check the users objects:
echo "User1: " . $User1->getGroup()->getName() . "n";
echo "User2: " . $User2->getGroup()->getName() . "n";
echo "User3: " . $User3->getGroup()->getName() . "n";

The output now is correct and as expected:

User1: My new group name
User2: My new group name
User3: My new group name

Now User1, User2 and User3 object instances are holding the same MyGroup instance with Id 1.
Hope this was easy to understand and I hope it will help you on your new project(s)!


Tags: , , ,

 
 
 
Content not available.
Please allow cookies by clicking Accept on the banner

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close