Miryafa Miryafa - 4 months ago 17
PHP Question

How can I use my session database with Zend's Session Manager?

When the Zend Session Manager from the tutorial starts a session, it generates a session key and posts a whole lot of data into the session. But I have a session system already set up with my own session keys and a different set of session data. How can I change the Zend configuration to use mine instead?

For reference, here is the Zend Session:

array (size=2)
'__ZF' =>
array (size=2)
'_REQUEST_ACCESS_TIME' => float 1468447555.1396
'_VALID' =>
array (size=3)
'Zend\Session\Validator\Id' => string 'xxxxxxxxxxxxxxxxxxxxxxxxxx' (length=26)
'Zend\Session\Validator\RemoteAddr' => string '--ip addr--' (length=13)
'Zend\Session\Validator\HttpUserAgent' => string '--user agent info--' (length=114)
'initialized' =>
object(Zend\Stdlib\ArrayObject)[371]
protected 'storage' =>
array (size=3)
'init' => int 1
'remoteAddr' => string '--ip addr--' (length=13)
'httpUserAgent' => string '--user agent info--' (length=114)
protected 'flag' => int 2
protected 'iteratorClass' => string 'ArrayIterator' (length=13)
protected 'protectedProperties' =>
array (size=4)
0 => string 'storage' (length=7)
1 => string 'flag' (length=4)
2 => string 'iteratorClass' (length=13)
3 => string 'protectedProperties' (length=19)


And here's what the session information I'm currently storing looks like (it's in a database, so I currently reference it with a Doctrine Entity):

object(MyModule\Entity\MySession)[550]
protected 'sessionid' => string 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (length=40)
protected 'data1' => string 'xxxxx' (length=5)
protected 'data2' => string 'xxxxxxxxxxxx' (length=12)
protected 'datatime' =>
object(DateTime)[547]
public 'date' => string '2016-07-13 17:05:52.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'xxxxxxxxxxxxxxx' (length=15)
protected 'data3' => boolean false
protected 'data4' => string '' (length=0)
protected 'data5' => int 9
protected 'data6' => int 17765
protected 'data7' => boolean false


My code for the session manager comes from this SO answer, so I'm providing a link rather than repasting it and cluttering up this question.

The reason I want to use Zend Session Manager rather than simply referencing my stored session information with Doctrine is so that I have a layer between the my program and the stored session information - so then I can change the way I access the session information without having to change my entire program.

Answer

I ended up solving this on my own by extending the SessionManager, SessionStorage, and SessionSaveHandler classes and rewriting some of the functionality. I also changed the Module.php and module.config.php files. This is what the changes look like:

module.config.php

<?php

/* ...required use statements... */

return array(
    'session' => array(
        'config' => array(
            'class' => 'Zend\Session\Config\SessionConfig',
            'options' => array(
                'name' => [my session name],
            ),
        ),
        'storage' => 'MySession\Model\MySessionStorage',
        'save_handler' => 'MySession\Model\MySessionSaveHandler'
    ),
    'service_manager' => array(
        'factories' => array(
            'session_service' => function($serviceManager) {
                $entityManager = $serviceManager->get('Doctrine\ORM\EntityManager');

                return new SessionService($entityManager, 'MySession');
            },
            'MySession\Model\MySessionSaveHandler' => function($serviceManager) {
                $sess = $serviceManager->get('onmysession_service');
                /* @var $adapter \Zend\Db\Adapter\Adapter */
                $adapter = $sm->get('Zend\Db\Adapter\Adapter');
                $tableGateway = new TableGateway('mytablename', $adapter);
                return new MySessionSaveHandler($tableGateway, new DbTableGatewayOptions(), $sess);
            },
            'MySessionManager' => function ($sm) {
                $config = $sm->get('config');
                if (isset($config['session'])) {
                    $session = $config['session'];

                    $sessionConfig = null;
                    if (isset($session['config'])) {
                        $class = isset($session['config']['class'])  ? $session['config']['class'] : 'Zend\Session\Config\SessionConfig';
                        $options = isset($session['config']['options']) ? $session['config']['options'] : array();
                        $sessionConfig = new $class();
                        $sessionConfig->setOptions($options);
                    }

                    $sessionStorage = null;
                    if (isset($session['storage'])) {
                        $class = $session['storage'];
                        $sessionStorage = new $class();
                    }

                    $sessionSaveHandler = null;
                    if (isset($session['save_handler'])) {
                        // class should be fetched from service manager since it will require constructor arguments
                        $sessionSaveHandler = $sm->get($session['save_handler']);
                    }

                    $sessionManager = new MySessionManager($sessionConfig, $sessionStorage, $sessionSaveHandler);
                } else {
                    $sessionManager = new MySessionManager();
                }
                MySession::setDefaultManager($sessionManager);
                return $sessionManager;
            },
        ),
    ),
    'db' => array(
        [db info here]
    ),
    /***************************************************************************************************************
     * Below is the doctrine configuration which holds information about the entities in this module and some
     * other doctrine stuff like orm drivers etc.
     ***************************************************************************************************************/
    'doctrine' => array(
        'driver' => array(
            'session_entities' => array(
                'class' =>'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
                'cache' => 'array',
                'paths' => array(__DIR__ . '/../src/MySession/Entity')
            ),
            'orm_default' => array(
                'drivers' => array(
                    'MySession\Entity' => 'session_entities'
                ),
            ),
        ),
    ),
);

Module.php

<?php

namespace MySession;

/* ...required use statements... */

/***************************************************************************************************
 * This class holds a few utility functions related to loading the module and accessing config
 * files for the module etc. These functions are primarily used by Zend under the hood.
 ***************************************************************************************************/
class Module implements AutoloaderProviderInterface, ConfigProviderInterface
{
    public function onBootstrap(MvcEvent $e) {
        $eventManager        = $e->getApplication()->getEventManager();

        // create the session manager
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
        $sessionManager = $e->getApplication()
                            ->getServiceManager()
                            ->get('MySessionManager');
        $sessionManager     ->start();

        // attach dispatch listener to validate user session
        $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($sessionManager, 'handleSessionValidation')); // TODO: we already handleSessionValidation on bootstrap, find out if it's necessary to do it on dispatch as well
    }

    /***************************************************************************************************
     * Returns the location of the module.config.php file. This function is used by the Zend Framework
     * underneath the hood.
     ***************************************************************************************************/
    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }

    /***************************************************************************************************
     * Returns the Zend StandardAutoLoader which contains the directory structure of the module source
     * folder.
     ***************************************************************************************************/
    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }
}

MySessionManager

<?php

namespace MySession\Model;

/* ...required use statements... */

class MySessionManager extends SessionManager
{
    /**
     * Is this session valid?
     *
     * A simple validation: checks if a row for the session name exists in the database
     *
     * @return bool
     */
    public function isValid()
    {
        $id = $_COOKIE[SessionVariableNames::$SESSION_NAME];
        return !is_null($this->getSaveHandler()->readMetadata($id));
    }

    /**
     * checks if the session is valid and dies if not.
     */
    public function handleSessionValidation() {
        if(stristr($_SERVER["SCRIPT_NAME"],"login.php"))
        {
            // we don't need to check the session at the login page
            return;
        }

        if (!$this->isValid()) {
            die("Not logged in.")
        }
    }

    /**
     * Start session
     *
     * If no session currently exists, attempt to start it. Calls
     * {@link isValid()} once session_start() is called, and raises an
     * exception if validation fails.
     *
     * @param bool $preserveStorage        If set to true, current session storage will not be overwritten by the
     *                                     contents of $_SESSION.
     * @return void
     * @throws RuntimeException
     */
    public function start($preserveStorage = false)
    {
        if ($this->sessionExists()) {
            return;
        }

        $saveHandler = $this->getSaveHandler();
        if ($saveHandler instanceof SaveHandlerInterface) {
            // register the session handler with ext/session
            $this->registerSaveHandler($saveHandler);
        }

        // check if old session data exists and merge it with new data if so
        $oldSessionData = [];
        if (isset($_SESSION)) {
            $oldSessionData = $_SESSION;
        }

        session_start();

        if ($oldSessionData instanceof \Traversable
            || (! empty($oldSessionData) && is_array($oldSessionData))
        ) {
            $_SESSION = ArrayUtils::merge($oldSessionData, $_SESSION, true); // this may not act like you'd expect, because the sessions are stored in ArrayObjects, so the second will always overwrite the first
        }

        $storage = $this->getStorage();

        // Since session is starting, we need to potentially repopulate our
        // session storage
        if ($storage instanceof SessionStorage && $_SESSION !== $storage) {
            if (!$preserveStorage) {
                $storage->fromArray($_SESSION);
            }
            $_SESSION = $storage;
        } elseif ($storage instanceof StorageInitializationInterface) {
            $storage->init($_SESSION);
        }

        $this->handleSessionValidation();
    }

    /**
     * Write session to save handler and close
     *
     * Once done, the Storage object will be marked as isImmutable.
     *
     * @return void
     */
    public function writeClose()
    {
        // The assumption is that we're using PHP's ext/session.
        // session_write_close() will actually overwrite $_SESSION with an
        // empty array on completion -- which leads to a mismatch between what
        // is in the storage object and $_SESSION. To get around this, we
        // temporarily reset $_SESSION to an array, and then re-link it to
        // the storage object.
        //
        // Additionally, while you _can_ write to $_SESSION following a
        // session_write_close() operation, no changes made to it will be
        // flushed to the session handler. As such, we now mark the storage
        // object isImmutable.
        $storage  = $this->getStorage();
        if (!$storage->isImmutable()) {
            $_SESSION = $storage->toArray(true);
            $this->saveHandler->writeMetadata(null, '_metadata');
            $this->saveHandler->writeData($_SESSION['_data']);
            session_write_close();
            $storage->fromArray($_SESSION);
            $storage->markImmutable();
        }
    }
}

MySessionStorage

<?php

namespace MySession\Model;

/* ...required use statements... */

class MySessionStorage extends SessionArrayStorage
{
    /**
     * Set storage metadata
     *
     * Metadata is used to store information about the data being stored in the
     * object. Some example use cases include:
     * - Setting expiry data
     * - Maintaining access counts
     * - localizing session storage
     * - etc.
     *
     * @param  string                     $key
     * @param  mixed                      $value
     * @param  bool                       $overwriteArray Whether to overwrite or merge array values; by default, merges
     * @return ArrayStorage
     * @throws Exception\RuntimeException
     */
    public function setMetadata($key, $value, $overwriteArray = false)
    {
        if ($this->isImmutable()) {
            throw new Exception\RuntimeException(
                sprintf('Cannot set key "%s" as storage is marked isImmutable', $key)
            );
        }

        // set the value
        $sessVar = $_SESSION['_metadata'];
        if (isset($sessVar[$key]) && is_array($value)) {
            // data is array, check if we're replacing the whole array or modify/add to it
            if ($overwriteArray) {
                $sessVar[$key] = $value;
            } else {
                $sessVar[$key] = array_replace_recursive($sessVar[$key], $value);
            }
        } else {
            // data is not an array, set or remove it in the session
            if ((null === $value) && isset($sessVar[$key])) {
                // remove data
                $array = $sessVar;
                unset($array[$key]);
                $_SESSION[SessionVariableNames::$SESSION_METADATA] = $array; // we can't use $sessVar here because it's only a copy of $_SESSION
                unset($array);
            } elseif (null !== $value) {
                // add data
                $sessVar[$key] = $value;
            }
        }

        return $this;
    }

    /**
     * Retrieve metadata for the storage object or a specific metadata key 
     * 
     * Looks at session db for the metadata
     *
     * Returns false if no metadata stored, or no metadata exists for the given
     * key.
     *
     * @param  null|int|string $key
     * @return mixed
     */
    public function getMetadata($key = null)
    {
        if (!isset($_SESSION)) {
            return false;
        }

        if (null === $key) {
            return $_SESSION;
        }

        if (!array_key_exists($key, $_SESSION)) {
            return false;
        }

        return $_SESSION[$key];
    }

    /**
     * Set the request access time
     *
     * @param  float        $time
     * @return ArrayStorage
     */
    protected function setRequestAccessTime($time)
    {
        // make a metadata write call, since that sets a timestamp
        $this->setMetadata('datatime', new DateTime("now"));

        return $this;
    }
}

MySessionSaveHandler

<?php

namespace MySession\Model;

/* ...required use statements... */

/**
 * This class is the back end of the $_SESSION variable, when used together with a SessionStorage and SessionManager in a ZF module
 */
class MySessionSaveHandler implements SaveHandlerInterface
{
    protected $sessionService;
    private $tableGateway;
    private $options;
    private $sessionName;
    private $sessionSavePath;
    private $lifetime;

    public function __construct(
        TableGateway $tableGateway,
        DbTableGatewayOptions $options,
        ISessionService $sessionService)
    {
        $this->tableGateway = $tableGateway;
        $this->options      = $options;
        $this->sessionService = $sessionService;
    }

    protected function getSessionService()
    {
        return $this->sessionService;
    }

    /**
     * Read session data
     *
     * @param string $id
     * @return string
     */
    public function read($id)
    {
        // Get data from database
        $metadata = $this->readMetadata($id);

        // Put data in PHP-session-serialized form
        $data = "_metadata|".serialize($metadata);
        return $data;
    }

    /**
     * Read session metadata
     *
     * @param string $id
     * @return mixed
     */
    public function readMetadata($id = null)
    {
        if (is_null($id))
        {
            if (!array_key_exists('sessionid', $_COOKIE))
            {
                // can't get id from cookie
                return null;
            }
            $id = $_COOKIE['sessionid'];
        }
        if ($data = $this->getSessionService()->findById($id))
        {
            return $data->getArrayCopy();
        }
        return null;
    }

    /** deprecated, use writeMetadata instead
     * Write session data
     *
     * @param string $id
     * @param string $data
     * @return bool
     * Note sessions use an alternative serialization method.
     */
    public function write($id, $data)
    {
        // don't use this because $data is serialized strangely and can't be automatically inserted into my table
    }

    /**
     * Write session metadata
     *
     * @param string $id
     * @param array $data an associative array matching a row in the table
     * @return mixed
     */
    public function writeMetadata($id = null, $data = null)
    {
        if (is_null($id))
        {
            if (!array_key_exists('sessionid', $_COOKIE))
            {
                // can't get id from cookie
                return null;
            }
            $id = $_COOKIE['sessionid'];
        }

        // get the session info from the database so we can modify it
        $sessionService = $this->getSessionService();
        $session = $sessionService->findByID($id);
        if (is_null($session)) {
            $session = new \MyModule\Entity\MySession();
        }
        if (!is_null($data))
        {
            // overwrite the stored data
            $session->setDataFromArray($data);
        }
        return $sessionService->save($session);
    }

    /**
     * Destroy session - deletes data from session table
     *
     * @param  string $id The session ID being destroyed.
     * @return bool
     * The return value (usually TRUE on success, FALSE on failure).
     * Note this value is returned internally to PHP for processing.
     */
    public function destroy($id)
    {
        $this->getSessionService()->delete($id);

        return true;
    }

    /**
     * Garbage Collection - cleanup old sessions
     *
     * @param int $maxlifetime
     * Sessions that have not updated for
     * the last maxlifetime seconds will be removed.
     * @return bool
     * The return value (usually TRUE on success, FALSE on failure).
     * Note this value is returned internally to PHP for processing.
     */
    public function gc($maxlifetime)
    {
        $metadata = $this->readMetadata(); // gets session id from cookie, then gets session from that
        if (!is_null($metadata))
        {
            $datatime = $metadata['datatime'];
            $previousTime = (new DateTime($datatime))->getTimestamp();

            // if (current time - datatime) > maxlifetime, destroy the session
            $val = time() - $previousTime;
            if ($val > $maxlifetime) {
                $this->destroy($metadata['sessionid']);
            }
        }
    }
}

The end result of all this is that you can access information stored in the database simply by accessing the $_SESSION variable, because the data gets loaded from the database into the $_SESSION variable on bootstrap, and the $_SESSION variable is written back into the database when the session is closed (which as I understand it, happens when the page is sent to the client).