zend-sessionでデータベースにセッション情報を保存する

zend-sessionでデータベースにセッション情報を保存する方法を忘備録的に纏めます。そして最終的にZendFramewrokにプルリクしてみましたよ、と言う話です。

セッションテーブル作成

こんな感じのテーブルを作成します。

CREATE TABLE `sessions` (
  `id` char(32) CHARACTER SET utf8 NOT NULL,
  `name` char(32) CHARACTER SET utf8 NOT NULL,
  `modified` int(11) DEFAULT NULL,
  `lifetime` int(11) DEFAULT NULL,
  `data` longtext CHARACTER SET utf8,
  PRIMARY KEY (`id`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

カラム名とかはこのままじゃなくても大丈夫ですが、デフォルトがこんな感じなので、そのまま使用することにします。

zend-sessionを有効にする

zend-sessionを有効にするには/config/modules.config.phpにZend\Sessionを追加します。今回セッションの保存先にデータベースを使用しますので、zend-dbも有効になってなかったら追加します。

<?php
return [
    'Zend\Db', // ← Zend\Dbを追加
    'Zend\Router',
    'Zend\Session', // ← Zend\Sessionを追加
    'Zend\Validator',
    'Application',
];

zend-sessionを有効にすることで、diコンテナからセッションマネージャーが取得出来るようになります。

データベース接続アダプタをサービスマネージャに登録する

以前の記事を参考にデータベース接続アダプタをサービスマネージャに登録してdiコンテナ経由で取得出来るようにておきます。

複数DB接続の設定が必要ない場合は/config/autoload/local.phpに下記の設定を追加すれば取得が可能になります。

<?php
use Zend\Db\Adapter;
return [
    'db' => [
        'driver'   => 'Mysqli',
        'database' => 'database_name',
        'username' => 'root',
        'password' => 'password',
        'hostname' => '192.168.1.1',
        'port'     => '',
        'charset'  => '',
    ],
    'service_manager' => [
        'factories' => [
            Adapter\Adapter::class => Adapter\AdapterServiceFactory::class,
        ],
        'aliases' => [
            'adapter' => Adapter\Adapter::class,
            'Adapter' => Adapter\Adapter::class,
        ],
    ],
];

セッションのセーブハンドラをサービスマネージャに登録する

セッションのセーブハンドラの情報をサービスマネージャに登録するには、/config/autoload/global.php(環境ごとに設定が変わる場合はlocal.phpでも構いません)に下記情報を追加します。

<?php
use Zend\Db\TableGateway\TableGateway;
use Zend\Session\Storage\SessionArrayStorage;
use Zend\Session\SaveHandler;
return [
    'service_manager' => [
        'session_config' => [
        ],
        'session_storage' => [
            'type' => SessionArrayStorage::class
        ],
        'factories' => [
            SaveHandler\SaveHandlerInterface::class => function ($container) {
                $adapter = $container->get('adapter');
                $sessionTable = new TableGateway('sessions', $adapter);
                $saveHandler = new SaveHandler\DbTableGateway(
                    $sessionTable,
                    new SaveHandler\DbTableGatewayOptions()
                );
                return $saveHandler;
            },
        ],
    ],
];

session_configはセッションの設定を行います。設定の変更を行う場合はこちらの情報を参考に変更してください。

session_strageはセッションへの格納方式を指定します。デフォルトはSessionArrayStorageなので、それをそのまま指定しています。 (省略するとエラーになります。なぜ。。。)

factoriesにSaveHandler\SaveHandlerInterfaceをキーにしてセーブハンドラを返すようにしてやることで、セッションマネージャをdiコンテナ経由で取得すると自動的にそのセーブハンドラを使用するようになります。今回はコールバックで直接global.phpにロジックを書き込みましたが、アプリケーションの設定(config/application.config.php)のconfig_cache_enabledが有効になっているとエラーになるので、factoryクラスを作成してそのクラス名を指定するほうがいいかも知れません。

セッションテーブルのカラム名がデフォルトと違うっていう場合は、SaveHandler\DbTableGatewayOptionsのインスタンスを作成して、セッターメソッドで設定することができます。

$options = new SaveHandler\DbTableGatewayOptions();
$options->setIdColumn('session_id');
$options->setNameColumn('session_name');
$saveHandler = new SaveHandler\DbTableGateway(
    $sessionTable,
    $options
);

セッションマネージャー取得

ここまでの設定を行えばdiコンテナからセッションマネージャーを取得しセッションを開始すれば、DBにセッションの情報が保存されているはずです。

$sessionManager = $container->get(\Zend\Session\SessionManager::class);
$sessionManager->start();

セッションマネージャー取得時にセッションコンテナのデフォルのマネージャーが設定されるので、いちいちセッションコンテナ作成時に、セッションマネージャーを引数で渡してやることもしなくていいです。

$session = new \Zend\Session\Container('hoge');

バグ?

しかしこのままMySQLを使用してセッションの有効期限が切れた場合、この様なエラーが出ました。

Commands out of sync; you can't run this command now

SaveHandler/DbTableGateway.phpのreadメソッドでセッションの有効期限が切れた場合に$this->destroy($id);してセッション情報を削除しているのですが、このdestroyメソッドが再度readメソッドを呼び出しているため、mysqli_result::freeされずにselectされてエラーになったものと思われます。

取りあえず、オーバーライドして独自実装で解決しようかと思ったのですが、ZendFramework3のバグかもと思って、一応プルリクして見ました。

英語とか、プルリクのお作法とか全くわからない状態なので、こうしたほうがいいよとかアドバイスがあったら是非いただきたいです。(っていうかバグなのか本当に?ってのもあるので、回避方法とかもあれば是非)

コメントを残す