Extending singletons in PHP
Asked Answered
S

8

29

I'm working in a web app framework, and part of it consists of a number of services, all implemented as singletons. They all extend a Service class, where the singleton behaviour is implemented, looking something like this:

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

Now, if I have a class called FileService implemented like this:

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

... calling FileService::getInstance() will not yield a FileService instance, like I want it to, but a Service instance. I assume the problem here is the "self" keyword used in the Service constructor.

Is there some other way to achieve what I want here? The singleton code is only a few lines, but I'd still like to avoid any code redundance whenever I can.

Surcharge answered 27/6, 2010 at 2:5 Comment(0)
L
61

Code:

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();

If you use PHP < 5.3, add this too:

// 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];
    }
}
Latifundium answered 27/6, 2010 at 2:32 Comment(9)
FYI: This code uses get_called_class, added in PHP 5.3. Doing this in earlier versions is a tad bit trickier.Bermejo
Holy yikes, that's scary. Imagine calling getInstance a dozen times, that's a dozen opens and a dozen reads of the class file.Bermejo
That's why people should upgrade to the latest and greatest ^^Latifundium
Thanks! I was actually looking at this function right after I posted the question, but I wasn't sure how to use it to solve the problem. Now I'll just have to wait until the web-hotel guys upgrade to PHP5.3 :)Surcharge
@Johan you might want to consider ditching the Singletons altogether. They introduce hard coupling to your application and are difficult to test. You can solve the may-have-only-one-instance issue with a Dependency Injection Framework or a Registry.Calendula
Does the extending class' constructor have to be public then?Pelerine
I've put a private getCalledClass() method in the Singleton class where I check if the get_called_class function exists (I want to put all code to be together). Am I doing wrong? It seems to work :)Simarouba
This doesn't work for me. It looks like the static $instances is being initialized to a new and empty array every time getInstance is calledHoulihan
You can also use static::class instead of get_called_class()Enginery
S
9

Had I paid more attention in 5.3 class, I would have known how to solve this myself. Using the new late static binding feature of PHP 5.3, I believe Coronatus' proposition can be simplified into this:

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() { }
}

I tried it out, and it works like a charm. Pre 5.3 is still a whole different story, though.

Surcharge answered 29/6, 2010 at 18:39 Comment(3)
It seems there only is a single field $instance for all subclasses, so only the singleton of the class where getInstance() is called first is returned.Pitchfork
This is the naive approach that unfortunately cannot work at all as C-Otto already mentioned. Downvoted: Don't do this at home ;-)Tyus
As they said above.. its wrong, this will give you the instance of the first class that used getInstance()Hypsography
B
4

I have found a good solution.

the following is my code

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) { }

You can refer http://php.net/manual/en/language.oop5.late-static-bindings.php for more info

Bombardon answered 19/6, 2018 at 6:52 Comment(3)
from which version is this supported?Grajeda
@ElteHupkes Did you find a proper solution?Mort
@YusufBayrak I was actually wrong (and I'm going to delete my comment), this answer does appear to work correctly if you don't forget to redefine the protected static $instance. I don't like that requirement (might as well use a trait at that point), so I'm personally using an array that tracks instances by fully specified class name. Still kind ugly, but at least it's fire and forget - it's only ugly once ;).Gentry
H
2

This is fixed Johan's answer. 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;
    }
}
Hypochondria answered 21/11, 2017 at 23:58 Comment(1)
This has the same problem as Johan's answer, at least in PHP 8.1. $instance is shared in the entire inheritance tree.Gentry
O
2

I came across this question cause I am using a Singleton class for managing a cache-like object and wanted to extend it. The answer by Amy B looked a bit too complicated for my taste so I dug a bit further and this is what I came up with, works like charm:

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();

Simply overriding the $instance class property fixes the issue. Only tested with PHP 8 but my guess is that this works for older versions as well.

Olsen answered 10/5, 2022 at 16:19 Comment(1)
It's the same as Martin Ding's solution besides simplified condition. I made a debug logging of Martin Ding's solution and find out that static::$instance is always has null or calling class value (PHP 7.4). So there is no need to check it's class. Thus your solution is better in that way. Though I think Amy B's solution is more easy to understand and it is not require child class to override the $instance property. A developer can easy forget about it.Signalment
C
0

Use trait instead of a abstract class allows to extend a singleton class.

Use the trait SingletonBase for a parent singleton class.

Use the trait SingletonChild for its singleton childs.

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

}
Cyrano answered 7/4, 2020 at 10:11 Comment(1)
If you extend that singleton with 2 different objects it will override the self::$instance with the latest child of the SingletonBaseMisbelief
M
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];
    }

The only issue with this one is, if you have the same name Singletons.

Misbelief answered 6/12, 2020 at 14:31 Comment(0)
S
0

An improved Amy B's solution without get_called_class() function

class Singleton
{
    protected static $instances = [];
    final private function __clone()
    {
    }

    public static function getInstance()
    {
        if (!isset(static::$instances[static::class])) {
            static::$instances[static::class] = new static();
        }
        return static::$instances[static::class];
    }
}
class A extends Singleton
{
}
class B extends Singleton
{
}

$a = A::getInstance();
$a->var = "I'm A";
$b = B::getInstance();
$c = A::getInstance();
var_dump($a, $b, $c);

It will show:

object(A)#1 (1) {
  ["var"]=>
  string(5) "I'm A"
}
object(B)#2 (0) {
}
object(A)#1 (1) {
  ["var"]=>
  string(5) "I'm A"
}  

Signalment answered 18/4 at 19:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.