TalkPHP

TalkPHP (http://www.talkphp.com/forums.php)
-   Advanced PHP Programming (http://www.talkphp.com/advanced-php-programming/)
-   -   A Generic Singleton Base Class (http://www.talkphp.com/advanced-php-programming/3248-generic-singleton-base-class.html)

Theo 08-17-2008 01:27 AM

A Generic Singleton Base Class
 
I'm from a C++ background and a Singleton hater. C++ just doesn't need them! However, I've found they're actually very useful for databases in PHP.

Anyways, I was trying to find a generic base class for Singletons in PHP. It's actually quite easy to do in C++ (which is probably why Singleton overload is a big problem). Most articles I find offer something like this:

PHP Code:

class Singleton
{
    
//    The sole access point for singletons.
    
final public static function GetInstance$inClass )
    {
        static 
$gInstances = array();
        if( !
array_key_exists$inClass$gInstances ) )
        {
            
self::$gInstances[$class] =& new $inClass;
        }       

        
$theInstance =& $gInstances[$inClass];
        return 
$theInstance;
    }


What on earth? That's not a singleton at all. It creates an array that can hold every concievable class. The array can only hold one of each class and has only a single point of access. However, the classes themselves can easily be instantiated again and/or cloned.

The cloning problem can be fixed by making the __clone function final and private in every class you want to be a singleton. Making __construct private prevents instantiation even by this class, not what you'd want at all.

Anyways, I can't really be bothered ensuring every class has a private clone function and constructor. That leads to errors. And even if they did, this class could still instantiate non-singletons as well.

So I came up with this:

PHP Code:

<?php

//------------------------------------------------------------------------------
//    Singleton
//        A class for managing singletons in the project.
//------------------------------------------------------------------------------

class Singleton
{
    
//    The sole access point for singletons.
    
final public static function GetInstance$inClass )
    {
        
//    Do we have one yet?
        
if( !array_key_exists$inClassself::$gInstances ) )
        {
            
self::$gInstances[$inClass] =& new $inClass;
            
            
//    Is this a singleton class?
            
if( !( self::$gInstances[$inClass] instanceof Singleton ) )
            {
                unset( 
self::$gInstances[$inClass] );
                throw new 
Exception"$inClass is not a Singleton class." );
            }
            
            
self::$gInstances[$inClass]->Construct();
        }

        
$theInstance =& self::$gInstances[$inClass];
        return 
$theInstance;
    }
    
    
//    Construct function - to be overrided by sub-classes if needed.
    
protected function Construct() {}

    
//    No creation or cloning.
    
final private function __construct() {}
    final private function 
__clone() {}
    
    
//    The instances of the class(es).
    
private static $gInstances = array();
}

?>

Singletons have to inherit from this class. If you call Singleton::GetInstance() with a class that isn't a Singleton, this function unsets it and throws an exception.

The Construct function is called the first time a Singleton is created. This can be overrided in sub-classes to do any initialisation the object may need.

The constructor and the clone function are declared final private - only accessible by this class and unavailable for overriding. Unfortunately, even if this constructor is private / protected this can be overrided by sub-classes as public if they are not final, which allows them to be instantiated more than once. This is the reason for the Construct() function.

If you create this test file:

PHP Code:

<?php

include_once( $_SERVER['DOCUMENT_ROOT'].'/../Include/Singleton.php' );

//    A test singleton with a 'Constructer'
class TestSingleton extends Singleton
{    
    function 
Construct()
    {
        echo 
"Constructing...<br />\n";
    }
    
    public 
$mVar;
}

//    A non-singleton class.
class NonSingleton {}

//    Instantiate a Singleton...
$theSingleton Singleton::GetInstance'Singleton' );

//    Instantiate the Test Singleton.
$theTestSingleton Singleton::GetInstance'TestSingleton' );

//    Assign some text to the member variable.
$theTestSingleton->mVar "Hello World!<br />\n";

//    Create another access point to the Test Singleton.
$theOtherTestSingleton Singleton::GetInstance'TestSingleton' );

//    Output the member variable.
echo $theOtherTestSingleton->mVar;

//    Try and instantiate a Non Singleton via Singleton.
$theNonSingleton Singleton::GetInstance'NonSingleton' );

?>

You get this output:

Quote:

Constructing...
Hello World!

Fatal error: Uncaught exception 'Exception' with message 'NonSingleton is not a Singleton class.' in /home/.sambrina/theoddman/Include/Singleton.php:22 Stack trace: #0 /home/.sambrina/theoddman/www.theoddman.com/test.php(31): Singleton::GetInstance('NonSingleton') #1 {main} thrown in /home/.sambrina/theoddman/Include/Singleton.php on line 22
As you can see, TestSingleton::Construct() only gets called once, despite us accessing it twice. We can also see that $TestSingleton and $OtherTestSingleton reference the same object.

When we try to create NonSingleton via Singleton, we get our exception as desired (which is uncaught in this case).

The only potential problems I can see in this are:

1) You can instantiate the Singleton class itself as demonstrated in the above code. This could be prevented by checking the input value, but I don't think it's worth it - surely only the worst coders would do this?

2) Construct() can be overrided to be public. This could be good and bad, I'm not sure how I feel about it...

3) Overhead?

Anyways, what do you guys think? Any other potential problems? Is this even gonna work? :)

M

Theo 08-17-2008 07:23 AM

I did think of another issue in this class. What if you want to pass in parameters on construction of a Singleton class? I'd argue against doing this as a Singleton is supposed to control its own construction, however it is possible to do:

PHP Code:

    //    The sole access point for singletons.
    
final public static function & GetInstance$inClass )
    {
        
//    Do we have one yet?
        
if( !array_key_exists$inClassself::$gInstances ) )
        {
            
self::$gInstances[$inClass] =& new $inClass;
            
            
//    Is this a singleton class?
            
if( !( self::$gInstances[$inClass] instanceof Singleton )
                || 
$inClass == 'Singleton' )
            {
                unset( 
self::$gInstances[$inClass] );
                throw new 
Exception"$inClass is not a Singleton class." );
            }
            
            
//    Get any extra arguments if any.
            
$theArguments func_get_args();
            
            
//    Remove the class name from the list.
            
array_shift$theArguments );
            
            
//    Call construct with any extra arguments passed in.
            
call_user_func_array
                
( array( self::$gInstances[$inClass], 'Construct' ),
                
$theArguments );
        }

        return 
self::$gInstances[$inClass];
    } 

We get the function arguments using func_get_args(). We then remove the first argument (this will be the class name). We then use call_user_func_array() to call the function and pass in any extra parameters passed into this function.

So with this test script:

PHP Code:

<?php

include_once( $_SERVER['DOCUMENT_ROOT'].'/../Include/Singleton.php' );

//    A test singleton with a 'Constructer'
class TestSingleton extends Singleton
{    
    function 
Construct$inVar1 null$inVar2 null )
    {
        echo <<<_TEXT_
Constructing...<br />
Var1 = 
$inVar1<br />
Var2 = 
$inVar2<br />
<br />
_TEXT_;
    }
    
    public 
$mVar;
}

//    Instantiate the Test Singleton.
$theTestSingleton Singleton::GetInstance'TestSingleton''Hello,''World!' );

?>

We get this output:

Quote:

Constructing...
Var1 = Hello,
Var2 = World!
A caveat is in order though: this kind of Singleton is created on first access. In more complicated scripts it can be difficult to determine exactly when a Singleton will be constructed. Always ensure that Singleton constructors have default values for all their parameters to prevent any warnings. Personally I'd choose to design my Singletons so they handle their own construction, which is kind of the point of Singletons anyway.

delayedinsanity 08-17-2008 07:32 AM

A much simpler way to pass multiple arguments via an array if you have Reflection available to you;

PHP Code:

$pReflect = new ReflectionClass($class);
self::$_registry[$class] = $pReflect->newInstanceArgs($args); 

-m

Theo 08-17-2008 09:24 AM

Unfortunately newInstanceArgs() calls the constructor, which cannot be overriden by any derived classes in this implementation. The default constructor could be rewritten, but we would then just be shifting the call to call_user_func_array() into the constructor.

delayedinsanity 08-17-2008 06:21 PM

I use a (probably bastardized) version of the registry pattern, which I understood to be an extension of the Singleton pattern, minus the addition of the getInstace() method in each class. So with the exception of a few variations in the pattern to incorporate factories, I make wide use of the constructor and wasn't thinking along the same line of what you're trying to present. I guess you can chock that up to the time of night. *!*

Wait Just A Minute... I still don't get it. What's wrong with calling the constructor in a singleton? You're calling a method called 'construct' which just seems hackish to make something similarily named anyways, isn't it? Why not use the constructor already available to the class - regardless, it's still being stored in a static location, and you can still run an initial check to make sure it's an instance of Singleton first.

I may not understand, I'm not very educated in design patterns, despite trying to learn, but it just seems to me there's no reason why not to use the constructor.
-m

Theo 08-17-2008 07:07 PM

The point is preventing anything else from creating another Singleton. A Singleton is supposed to guarantee that only one instance of the singleton class exists. Ever.

There's nothing wrong with it per se, it's a limitation of this particular implementation. The base Singleton constructor is declared final, so it cannot be overriden. The upshot of which is that any class that extends Singleton cannot override the constructor. This is to prevent someone from creating a subclass that overrides the constructor and makes it public, thus breaking the Singleton pattern. In C++ this can't happen, if a base class has a private constructor then all derived classes can only instantiate themselves. __construct() is still called, it just can't be overridden to take parameters or do anything else.

This is the reason for the Construct() function that can be overridden in sub-classes if the singleton in question requires some kind of initialisation. Although perhaps a semantically better name for the function would have been Init().

This of course leads to the problem of: what if I want to pass in parameters to a singleton constructer. My second post above was my best effort to solve that problem. With that change, you just pass in the extra parameters after the class name when you call GetInstance().

I'm beginning to regret that second post now. Singletons created on first access should control their own construction. The simplest way to do this is to keep all constructors parameter free.

Of course you could decide to not declare the constructor final, but you then have to remember that every class derived from Singleton needs a private constructor to work properly.

To show by example if we do this instead:

PHP Code:

class Singleton
{
...
    
//    No creation or cloning.
    
private function __construct() { $this->Construct(); }
    final private function 
__clone() {}
...


We can also do this:

PHP Code:

<?php

include_once( $_SERVER['DOCUMENT_ROOT'].'/../Include/Singleton.php' );

//    A test singleton with a 'Constructer'
class TestSingleton extends Singleton
{
    public function 
__construct$inVar )
    {
        
$this->mVar $inVar;
    }
    
    public 
$mVar;
}

//    Instantiate the Test Singleton.
$theTestSingleton Singleton::GetInstance'TestSingleton''Hello World!' );

$theOtherSingleton = new TestSingleton'Hi Other World!' );
echo 
$theTestSingleton->mVar.'<br />';
echo 
$theOtherSingleton->mVar;

?>

This gives the output:

Quote:

Hello World!
Hi Other World!
As you can see we have two instances of the class created, and it is therefore not a Singleton.

Theo 08-17-2008 07:23 PM

By the way, I've just looked up that registry pattern and I think I like it! :)

Kalle 08-18-2008 02:25 AM

I personally use a mix of the Singleton and Registry pattern with some property overloading.

Example:
PHP Code:

<?php
    
final class Project
    
{
        protected static 
$instance;
        protected static 
$objects    = Array();


        private function 
__construct()
        {
        }

        private function 
__clone()
        {
        }

        public function 
__get($component)
        {
            
$component strtolower((string) $component);

            if(
array_key_exists($componentself::$objects))
            {
                return(
self::$objects[$component]);
            }

            return(
NULL);
        }

        public static function 
init()
        {
            if(!
is_object(self::$instance))
            {
                
self::$instance = new self;
            }

            return(
self::$instance);
        }

        public static function 
load($component)
        {
            
$component strtolower((string) $component);

            if(
array_key_exists($componentself::$objects))
            {
                return(
true);
            }

            
$class 'Project_Component_' $component;

            if(!
class_exists($class))
            {
                throw new 
Project_Exception('Component class not found!');
            }

            
self::$objects[$component] = new $class;

            return(
self::$objects[$component]);
        }
    }

    class 
Project_Exception extends Exception
    
{
    }
?>

This does not introduce component constructor arguments, but its simply a mix. I hope this could help you abit ;)


All times are GMT. The time now is 03:58 PM.

Powered by vBulletin® Version 3.6.8
Copyright ©2000 - 2013, Jelsoft Enterprises Ltd.
Search Engine Optimization by vBSEO 3.1.0