Пользовательский сеанс SaveHandler в zf3 с Doctrine

Как реализовать сеансы в базе данных при использовании ZF3 и Doctrine?

Руководство говорит:

Могут быть случаи, когда вы хотите создать обработчик сохранения, который в настоящее время не существует. Создание пользовательского обработчика сохранения очень похоже на создание пользовательского обработчика сохранения PHP. Все обработчики сохранения должны реализовывать Zend\Session\SaveHandler\SaveHandlerInterface. Как правило, если у вашего обработчика сохранения есть параметры, вы создадите другой класс параметров для конфигурации обработчика сохранения.

Я попытался создать собственный класс, реализующий этот интерфейс, но получаю следующую ошибку:

expects a class implementing Zend\Session\Storage\StorageInterface'

с этим конфигом:

 'session_storage' => [
//        'type' => SessionArrayStorage::class (with array storage works ok)
        'type' => \Application\Session\SaveHandler\Doctrine::class (tried to implement suggested interface)
    ],

Обратите внимание, что в руководстве предлагается SaveHandlerInterface, но ожидается StorageInterface.

Любой пример, как это сделать?

Изменить:

Моя текущая реализация.

In global.php:

  'session_config' => [
        // Session cookie will expire in 1 hour.
        'cookie_lifetime' => 60*60*1,
        // Session data will be stored on server maximum for 30 days.
        'gc_maxlifetime'     => 60*60*24*30,
    ],
    // Session manager configuration.
    'session_manager' => [
        // Session validators (used for security).
        'validators' => [
            RemoteAddr::class,
            HttpUserAgent::class,
        ]
    ],
    // Session storage configuration.
    'session_storage' => [
        'type' => \Application\Session\Storage\Doctrine::class,
    ],
    'session_containers' => [
        'UserSession'
    ]

in Module.php:

/**
     * This method is called once the MVC bootstrapping is complete.
     */
    public function onBootstrap(MvcEvent $event)
    {
        $application = $event->getApplication();
        $serviceManager = $application->getServiceManager();



        // The following line instantiates the SessionManager and automatically
        // makes the SessionManager the 'default' one
        /** @var SessionManager $sessionManager */
        $sessionManager = $serviceManager->get(SessionManager::class);

        $entityManager =  $serviceManager->get('doctrine.entitymanager.orm_default');

        /** @var Doctrine $storage */
        $storage = $sessionManager->getStorage();
        $storage->setEntityManager($        
    }

in Application\Session\Storage\Doctrine.php:

class Doctrine implements
    IteratorAggregate,
    StorageInterface,
    StorageInitializationInterface
{
    public function setEntityManager($em) {  
        $this->entityManager = $em;
    }

    // ...
    // other functions as required by interfaces
}

Это работает, но недостаток в том, что Doctrine Storage будет доступен только в этом модуле, и я специально ввожу его при каждом запросе (Boostrap), а не тогда, когда это действительно нужно (Factory).

**Обновлять: **

Я написал SaveHandler, но похоже значения не сохраняются после запросов.

Вот код:

    class Doctrine extends ArrayStorage implements SaveHandlerInterface {

    /**
     * @param string $session_id
     * @return string Encdoded session data string
     */
    public function read($session_id)
    {
        $entity = $this->getEntity($session_id);
        if ($entity) {
            return $entity->getSessionData();
//          sample output:
//          string '__ZF|a:2:{s:20:"_REQUEST_ACCESS_TIME";d:1501933765.497678;s:6:"_VALID";a:3:{s:25:"Zend\Session\Validator\Id";s:26:"3kr15rhi6ijhneu7rruro9gr76";s:33:"Zend\Session\Validator\RemoteAddr";s:9:"127.0.0.1";s:36:"Zend\Session\Validator\HttpUserAgent";s:133:"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36";}}FlashMessenger|C:23:"Zend\Stdlib\ArrayObject":205:{a:4:{s:7:"storage";a:0:{}s:4:"flag";i:2;s:13:"iteratorClass";s:13:"ArrayI'... (length=645)
//          note that counter is not present            
        }
    }

    /**
     * @param string $session_id
     * @param string $session_data Encoded session data string
     * @return bool
     */
    public function write($session_id, $session_data)
    {
//        sample input ($session_data):
//        string '__ZF|a:2:{s:20:"_REQUEST_ACCESS_TIME";d:1501934933.9573331;s:6:"_VALID";a:3:{s:25:"Zend\Session\Validator\Id";s:26:"3kr15rhi6ijhneu7rruro9gr76";s:33:"Zend\Session\Validator\RemoteAddr";s:9:"127.0.0.1";s:36:"Zend\Session\Validator\HttpUserAgent";s:133:"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36";}}UserSession|C:23:"Zend\Stdlib\ArrayObject":223:{a:4:{s:7:"storage";a:1:{s:7:"counter";i:1;}s:4:"flag";i:2;s:13:"iteratorCla'... (length=918)
//        (note that counter variable is set)

        $entity = $this->getEntity($session_id);

        $entity->setSessionData($session_data);
        $entity->setLifetime($this->getLifeTime());

        $this->getEntityManager()->persist($entity);
        $this->getEntityManager()->flush($entity);

        return true;
    }

    /**
     * @param string $session_id
     * @return Entity|null
     */
    public function getEntity($session_id)
    {
        $this->entity = $this->getRepository()->find($session_id);

        if (!$this->entity) {
            $this->entity = new $this->entityName;
            $this->entity->setId($session_id);
        }

        return $this->entity;
    }

    //  ....

 }

person takeshin    schedule 30.07.2017    source источник
comment
Когда именно вы получаете эту ошибку?   -  person akond    schedule 31.07.2017
comment
Я думаю, когда служба сеанса зарегистрирована в диспетчере сеансов.   -  person takeshin    schedule 31.07.2017
comment
Я написал очень простой фрагмент POC с тривиальной реализацией SaveHandlerInterface. Ошибок пока нет.   -  person akond    schedule 31.07.2017
comment
@akond, как вы внедряете зависимости (например, EntityManager) в SaveHandler? Не могли бы вы поделиться своим кодом?   -  person takeshin    schedule 31.07.2017
comment
Я не вводил никакой зависимости. Просто старые добрые new. Было бы лучше, если бы вы показали свой код.   -  person akond    schedule 31.07.2017


Ответы (2)


На самом деле вам нужно реализовать эти интерфейсы, потому что SaveHandlerInterface нужен для PHP, а StorageInterface для ZF3. Ваш обработчик хранилища является своего рода шлюзом между ними.

Это должно работать. Вы можете внедрить все зависимости внутри фабрики.

Application/src/DoctrineSaveHandler.php

namespace Application;

use Zend\Session\SaveHandler\SaveHandlerInterface;
use Zend\Session\Storage\ArrayStorage;

class DoctrineSaveHandler extends ArrayStorage implements SaveHandlerInterface
{
    public function close () {}
    public function destroy ($session_id) {}
    public function gc ($maxlifetime) {}
    public function open ($save_path, $name)  {}
    public function read ($session_id)  {}
    public function write ($session_id, $session_data)  {}
}

config/autoload/global.php

"service_manager" => [
    'aliases' => [
        \Zend\Session\SaveHandler\SaveHandlerInterface::class => \Zend\Session\Storage\StorageInterface::class
    ],
    'factories' => [
        \Zend\Session\Storage\StorageInterface::class => function () {
            // -------------------------------
            // YOU NEED A PROPER FACTORY HERE!
            // -------------------------------
            return new DoctrineSaveHandler();
        },
    ]
]
person akond    schedule 01.08.2017
comment
Это позволило мне внедрить EntityManager, но как только я реализовал методы read() и write(), закодированные значения сохраняются в базе данных, но не обрабатываются должным образом (контейнер сеанса вообще не имеет значений). Вероятно, есть некоторые проблемы с сериализацией/десериализацией данных. Я обновил вопрос с моим текущим кодом. - person takeshin; 05.08.2017

Честно говоря, я не использовал доктрину с обработчиком сохранения для управления сеансом. Но позвольте мне рассказать вам о том, как должна быть построена каждая часть Zend\Session, особенно для SessionManager::class.

SessionArrayStorage::class реализует Zend\Session\Storage\StorageInterface и используется для хранения данных сеанса в пользу SessionManager::class.

На самом деле эта часть Zend\Session делает большую вещь. Он работает вместо $_SESSION superglobal и использует ArrayObject::class из Zend\Stdlib. Это даст вам большую гибкость, означающую, что вы сможете использовать следующие функции: доступ к свойствам, хранение метаданных, блокировку и неизменность. (Честно говоря, я не использовал их все).

'session_storage' => [
    // 'type' => SessionArrayStorage::class (with array storage works ok)
    'type' => \Application\Session\SaveHandler\Doctrine::class (tried to implement suggested interface)
],

Теперь дело в том, что вы используете собственный обработчик сохранения вместо хранилища, что неправильно. Поскольку обработчик сохранения не реализует Zend\Session\Storage\StorageInterface. Вот почему вы получаете этот тип ошибки.

Обработчик сохранения обычно используется для хранения данных сеанса в базе данных, файлах или системах кэширования. Поскольку вы создаете собственный обработчик сохранения, это означает, что вы реализуете Zend\Session\SaveHandler\SaveHandlerInterface. Поэтому приходится работать с open($savePath, $name), read($id), write($id, $data), destroy($id) и так далее.

Таким образом, чтобы полностью настроить SessionManager::class для управления сеансом, вам необходимо предоставить в нем три вещи: конфигурацию сеанса, хранилище сеанса и обработчик сохранения. Например

$sessionManager = new SessionManager(
    $sessionConfig,
    $sessionStorage,
    // provide your save handler here
    $sessionSaveHandler
);

// this keeps this configuration in mind in later calls of 'Container::class'
Container::setDefaultManager($sessionManager);
return $sessionManager;

Теперь мы настроили SessionManager::class. Мы могли бы вызвать это, когда нам это нужно. Например, после того, как кто-то подтвердил свои учетные данные для входа.

$session = $e->getApplication()
    ->getServiceManager()
    ->get(SessionManager::class);
$session->start();

После этого мы сможем использовать Container::class часть компонента Zend\Session следующим образом:

// this would use the above configuration 
$container = new Container('initialized');

$session->getSaveHandler()->open('path', 'session-name');

// Write data to db, files etc
// "$contents" must be serialized data; coentents can be: id, email etc
$session->getSaveHandler()->write($session->getId(), $contents);

// Read data
$storedData = $session->getSaveHandler()->read($session->getId());

Теперь мы сможем использовать любое пользовательское свойство и хранить такие значения, как

$container->storedData = $storedData;
$container->remoteAddr = 127.0.0.1;

Далее, где нам нужно получить эти значения, мы можем получить их таким образом

$container = new Container('initialized');
print_r($container->storedData);

//or
echo $container->remoteAddr;

Надеюсь, это поможет вам немного!

person unclexo    schedule 31.07.2017
comment
Спасибо, но вопрос в том, как внедрить зависимость EntityManager в пользовательский класс хранилища сеансов (имя которого вы можете установить, изменив type в конфигурации global.php), чтобы он был доступен во всех модулях приложения. Пока я делаю это в Module.php, получая экземпляр хранилища и передавая диспетчер сущностей. Но таким образом я ввожу полный объект, а не фабрику, и этот объект доступен только в текущем модуле. Как внедрить фабрику EntityManager в менеджер сеансов? - person takeshin; 31.07.2017
comment
Ну, сначала вам нужно создать фабрики для ваших Doctrine::class и EntityManager::class. Затем введите EntityManager::class в метод установки Doctrine::class при создании фабрики. Затем используйте сервис-менеджер для вызова их туда, где вам нужно. Спасибо - person unclexo; 01.08.2017