Search code examples
phpsessionphpunitglobal-variablestestability

Building a more testable session manager


I'm working on a set of components (that will hopefully become a full framework) and am currently working on one to provide an abstraction of PHP sessions.

I'm trying to make the code as testable as possible, but a session class, by definition, is going to rely on global state in the form of the $_SESSION superglobal.

I've tried to implement my session class in such a way that $SESSION and session* functions only get called in one place, which I can then override in PHPUnit for testing purposes, but I can't help but wonder if there's a better way of doing this.

If you can suggest a better approach to making a testable session class then I'd appreciate any input you may have.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

namespace gordian\reefknot\storage\session;

use gordian\reefknot\storage;

/**
 * Session management
 * 
 * Each instance of the Session object represents a "namespace" within the PHP
 * $_SESSION system.  This allows sessions to be easily managed and organized
 * within an application
 * 
 */
class Session implements storage\iface\Crud, iface\Session
{

    protected
        $name       = '',
        $storage    = NULL;


    /**
     * Add a new item to the session
     * 
     * @param mixed $data 
     * @param string $key
     * @return Session
     * @throws \InvalidArgumentException Thrown if no name is provided
     */
    public function createItem ($data, $key)
    {
        if (!empty ($key))
        {
            $key    = (string) $key;
            if (($this -> storage === NULL)
            || (!array_key_exists ($key, $this -> storage)))
            {
                $this -> storage [$key] = $data;
            }
        }
        else
        {
            throw new \Exception ('No valid key given');
        }
        return ($this);
    }

    /**
     * Delete the specified key
     * 
     * @param string $key 
     * @return Session
     */
    public function deleteItem ($key)
    {
        unset ($this -> storage [$key]);
        return ($this);
    }

    /**
     * Retrieve the data stored in the specified key
     * 
     * @param type $key 
     * @return mixed
     */
    public function readItem ($key)
    {
        return (array_key_exists ($key, $this -> storage)? 
            $this -> storage ['key']: 
            NULL);
    }

    /**
     * Update a previously stored data item to a new value
     * 
     * @param mixed $data 
     * @param string $key
     */
    public function updateItem ($data, $key)
    {
        if ($this -> storage === NULL)
        {
            throw new \RuntimeException ('Session contains no data');
        }

        if (array_key_exists ($key, $this -> storage))
        {
            $this -> storage [$key] = $data;
        }
        return ($this);
    }

    /**
     * Clear the session of all stored data
     * 
     * @return Session 
     */
    public function reset ()
    {
        $this -> storage = NULL;
        return ($this);
    }

    /**
     * Retrieve all data stored in the session
     * 
     * @return array 
     */
    public function getAll ()
    {
        return ($this -> storage);
    }

    /**
     * Return whether there is data stored in this session
     * 
     * @return bool 
     */
    public function hasData ()
    {
        return (!empty ($this -> storage));
    }

    /**
     * Initialize the back-end storage for the session
     * 
     * This method provides access for this class to the underlying PHP session
     * mechanism.  
     * 
     * @return bool Whether the newly initialized session contains data or not
     * @throws \RuntimeException Will be thrown if the session failed to start
     */
    protected function initStorage ()
    {
        // Check that storage hasn't already been initialized
        if ($this -> storage === NULL)
        {
            // Attempt to start the session if it hasn't already been started
            if ((session_id () === '')
            && ((headers_sent ()) 
            || ((!session_start ()))))
            {
                throw new \RuntimeException ('Unable to start session at this time');
            }
            // Alias our instance storage to the named $_SESSION variable
            $this -> storage    =& $_SESSION [$this -> name];
        }
        return ($this -> hasData ());
    }

    /**
     * Class constructor
     * 
     * @param string $sessName
     * @throws \InvalidArgumentException Thrown if no session name is provided
     */
    public function __construct ($sessName)
    {
        if (!empty ($sessName))
        {
            $this -> name   = $sessName;
            $this -> initStorage ();
        }
        else
        {
            throw new \InvalidArgumentException ('Session must have a name');
        }
    }
}

For testing, the current plan is to replace initStorage() with a method that just sets up an internal array instead. If you can suggest a better approach I'd be keen to hear it.


Solution

  • if i undestand correctly..

    create an abstraction of the native session management so that your session storage helper doesn't need to actually do any of the session_* calls or access $_SESSION directly.

    Have two implementations of it, one that actually does the right thing, the other than fakes session_*() and $_SESSION, and in your constructor you just call SESSIONCLASS::start() and SESSIONCLASS::getVar(name). then you can test "Session" completely.