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?