Stealth PHP Singletons
I explained a bit of my code in a post up on SitePoint that reminded me of one of my favorite bag of php tricks: associating a superglobal key reference with a class variable.
<?php
$this->_stack =& $_SESSION[NOTIFY_SESSION_KEY];
?>
This has the effect of making a “stealth” static member, as well as effectively making the class a singleton (i.e. every instance of the class will access and manipulate the same data, and is therefore equivalent).
The context of my post was explaining a session based user notification queue. I have been using WACT recently, so I included a helper method to return the queue as an ArrayDataSet.
So without further ado, here is the test case and code.
<?php
require_once 'models/NotifyStack.php';
if (!array_key_exists('_SESSION',$GLOBALS)) {
session_start();
}
/**
* Test Unit Case for NotifyStack model
* @package NotifyStackMaint
* @subpackage tests
*/
class TestNotifyStack extends UnitTestCase
{
/**
* constructor
* @return void
*/
function TestNotifyStack($psName='')
{
$this->UnitTestCase($psName);
}
function SetUp() {}
function TearDown() {}
/**#@+
* test case
* @return void
*/
function TestEnv() {
$this->assertTrue(defined('NOTIFY_SESSION_KEY'));
}
function TestClass() {
$this->assertTrue(class_exists('NotifyStack'),'NotifyStack class exists');
$o =& new NotifyStack;
$this->assertNoErrors();
}
function TestFetchArray() {
$o =& new NotifyStack;
$this->assertTrue(method_exists($o, 'fetchArray'), 'fetchArray method exists');
$this->assertIsA($ret = $o->fetchArray(), 'array', 'fetchArray returns an array');
}
function TestFetchDataSet() {
$o =& new NotifyStack;
$this->assertTrue(method_exists($o, 'fetchDataSet'), 'fetchDataSet method exists');
$this->assertIsA($ret =& $o->fetchDataSet(), 'arraydataset', 'fetchDataSet returns an ArrayDataSet');
// adheres to iterator interface
foreach(array('reset','next','get','export') as $method) {
$this->assertTrue(method_exists($ret,$method), $method.' exists in returned object');
}
}
function TestBadPush() {
$o =& new NotifyStack;
$this->assertTrue(method_exists($o, 'push'), 'push method exists');
$o->push();
$this->assertError('Missing argument 1 for push()');
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertEqual(0, count($ret));
}
function TestValidPush() {
$o =& new NotifyStack;
$o->push('test');
$this->assertNoErrors();
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertTrue(array_key_exists(0,$ret),'returned array has a 0 index');
foreach(array('message','title','style') as $key) {
$this->assertTrue(array_key_exists($key,$ret[0]),'returned array item 0 has a '.$key.' index');
}
$this->assertEqual('test',$ret[0]['message']);
}
function TestFetchClearsStack() {
$o =& new NotifyStack;
$o->push('test');
$this->assertNoErrors();
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertTrue(array_key_exists(0,$ret),'returned array has a 0 index');
$this->assertTrue(array_key_exists('message',$ret[0]),'returned array item 0 has a message index');
$this->assertEqual('test',$ret[0]['message']);
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertEqual(0, count($ret));
}
function TestPushTitle() {
$o =& new NotifyStack;
$o->push('test','tt');
$this->assertNoErrors();
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertTrue(array_key_exists(0,$ret),'returned array has a 0 index');
foreach(array('message','title','style') as $key) {
$this->assertTrue(array_key_exists($key,$ret[0]),'returned array item 0 has a '.$key.' index');
}
$this->assertEqual('tt',$ret[0]['title']);
}
function TestPushStyle() {
$o =& new NotifyStack;
$o->push('test','title',NOTIFY_WARN_STYLE);
$this->assertNoErrors();
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertTrue(array_key_exists(0,$ret),'returned array has a 0 index');
$this->assertEqual(NOTIFY_WARN_STYLE,$ret[0]['style']);
$o->push('test','title',NOTIFY_FATAL_STYLE);
$this->assertNoErrors();
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertTrue(array_key_exists(0,$ret),'returned array has a 0 index');
$this->assertEqual(NOTIFY_FATAL_STYLE,$ret[0]['style']);
}
function TestPushBadStyle() {
$o =& new NotifyStack;
$o->push('test','title',-243234);
$this->assertNoErrors();
$this->assertIsA($ret = $o->fetchArray(), 'array');
$this->assertTrue(array_key_exists(0,$ret),'returned array has a 0 index');
$this->assertEqual(NOTIFY_INFO_STYLE,$ret[0]['style']);
}
/**#@-*/
}
?>
<?php
/**#@+
* user notification constants
*/
/**
* session identifier for stack data
*/
define('NOTIFY_SESSION_KEY','_NOTIFY_STACK_KEY_');
define('NOTIFY_INFO_STYLE',1);
define('NOTIFY_WARN_STYLE',2);
define('NOTIFY_FATAL_STYLE',4);
/**#@-*/
/**
* encapsulate user notification queueing manipulation
*
* @package CdpMaint
* @subpackage models
*/
class NotifyStack {
/**#@+
* @private
*/
/**
* @var array stack info
*/
var $_stack;
/**#@-*/
/**
* constructor
* @return void
*/
function NotifyStack() {
if (!array_key_exists(NOTIFY_SESSION_KEY, $_SESSION)) {
$_SESSION[NOTIFY_SESSION_KEY] = array();
}
$this->_stack =& $_SESSION[NOTIFY_SESSION_KEY];
}
/**
* clear the stack
* @return void
*/
function _reset() {
$_SESSION[NOTIFY_SESSION_KEY] = array();
$this->_stack =& $_SESSION[NOTIFY_SESSION_KEY];
}
/**
* return user notifications as an array
* @return void
*/
function fetchArray() {
$ret = $this->_stack;
$this->_reset();
return $ret;
}
/**
* return user notifications as an ArrayDataSet
* @return void
*/
function &fetchDataSet() {
return new ArrayDataSet($this->fetchArray());
}
/**
* ensures the style is a valid NOTIFY style constant
* @param integer source style input
* @retur integer valid style constant value
*/
function _validateStyle($style) {
switch((int)$style) {
case NOTIFY_WARN_STYLE:
case NOTIFY_FATAL_STYLE:
return (int)$style;
break;
default:
return NOTIFY_INFO_STYLE;
}
}
/**
* add a user notification
* @return void
*/
function push($msg, $title=false, $style=NOTIFY_INFO_STYLE) {
if (strlen($msg) > 0) {
$this->_stack[] = array(
'message' => $msg
,'title' => $title
,'style' => $this->_validateStyle($style)
);
}
}
}
?>
I would be curious to anyone else’s opinion of the “stealth singleton” effect: good, bad or ugly?
I am using the same technique, but I use $GLOBALS instead $SESSION, like this in the constructor :
$class = get_class($this);
if (isset($GLOBALS[$class]) == true && get_class($GLOBALS[$class]) == $class)
$this = & $GLOBALS[$class];
else
$GLOBALS[$class] = & $this;
This code doest not work with PHP 5 (use static properties instead).
Best regards, Fred
If my goal is simply to implement a singleton, I would tend to use a static inside of a getInstance() method myself in php4. i.e.
class FooManager {
function &getInstance() {
static $foo = false;
if (!$foo) $foo =& new Foo;
return $foo;
}
}
class Singleton
{
function createInstance($singletonClassName)
{
if (!isset($GLOBALS['_singleton'])) {
$GLOBALS['_singleton'] = array();
}
$GLOBALS['_singleton'][strtolower($singletonClassName)] =& new $singletonClassName;
}
function & getInstance($singletonClassName)
{
if (!isset($GLOBALS['_singleton'][strtolower($singletonClassName)])) {
Singleton::createInstance($singletonClassName);
}
return $GLOBALS['_singleton'][strtolower($singletonClassName)];
}
}
class FooManager
{
}
$foo =& Singleton::getInstance('FooManager');
Basically it’s the same trick, but i place the factory method outside the actual singleton-class. The main benifit is that if I ever decide to start using my singletonclass as a normal (multiple instances) class, i wouldn’t have to change the code.