如何将我的会话数据库与 Zend 的会话管理器一起使用


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

当教程中的 Zend 会话管理器启动会话时,它会生成一个会话密钥并将大量数据发布到会话中。但是我已经使用自己的会话密钥和一组不同的会话数据设置了一个会话系统。如何更改 Zend 配置以改用我的配置?

作为参考,这里是 Zend 会话:

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)

以下是我当前存储的会话信息的样子(它在数据库中,所以我目前使用教义实体引用它):

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

我的会话管理器代码来自这个 SO 答案,所以我提供了一个链接,而不是重新粘贴它并使这个问题变得混乱。

我想使用 Zend 会话管理器而不是简单地使用 Doctrine 引用我存储的会话信息的原因是,在我的程序和存储的会话信息之间有一个层 - 这样我就可以更改访问会话信息的方式,而不必更改我的整个程序。

我最终通过扩展 SessionManager、SessionStorage 和 SessionSaveHandler 类并重写一些功能来解决这个问题。我还更改了 Module.php 和 module.config.php 文件。以下是更改的外观:

模块配置.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'
                ),
            ),
        ),
    ),
);

模块.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__,
                ),
            ),
        );
    }
}

我的会话管理器

<?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();
        }
    }
}

我的会话存储

<?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']);
            }
        }
    }
}

所有这一切的最终结果是,您只需访问 $_SESSION 变量即可访问存储在数据库中的信息,因为数据在引导时从数据库加载到 $_SESSION 变量中,并且 $_SESSION 变量在会话关闭时写回数据库(据我了解, 在将页面发送到客户端时发生)。