如何将我的 session 数据库与 Zend 的 Session 管理器一起使用?

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

Zend Session Manager from the tutorial 启动 session 时,它会生成一个 session 密钥并将大量数据发送到 session。但是我有一个 session 系统已经设置了我自己的 session 键和一组不同的 session 数据。我如何更改 Zend 配置以改为使用我的配置?

作为参考,这里是 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)

这是我当前存储的 session 信息的样子(它在数据库中,所以我目前用 Doctrine 实体引用它):

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

我的 session 管理器代码来自 ,所以我提供 link 而不是重新粘贴它并弄乱这个问题。

我想使用 Zend Session Manager 而不是简单地用 Doctrine 引用我存储的 session 信息的原因是我的程序和存储的 session 之间有一个层] 信息 - 这样我就可以更改访问 session 信息的方式,而无需更改我的整个程序。

我最终通过扩展 SessionManager、SessionStorage 和 SessionSaveHandler 类 并重写了一些功能自行解决了这个问题。我还更改了 Module.php 和 module.config.php 文件。这是变化的样子:

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

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