25.3. 高度な使用法
前のページ | 第25章 Zend_Session | 次のページ

## 25.3. 高度な使用法

基本的な使用法の例で Zend Framework のセッションを完全に使用することができますが、 よりよい方法もあります。 `Zend_Auth` のサンプル を見てみましょう。これは、デフォルトで Zend_Session_Namespace を使用することにより、 認証トークンを持続的に保持している例です。 この例は、Zend_Session_Namespace および Zend_Auth を手早く簡単に統合するためのひとつの方法を示すものです。

### 25.3.1. セッションの開始

すべてのリクエストで Zend Framework のセッションを使用してセッション管理したい場合は、 起動ファイルでセッションを開始します。

例 25.6. グローバルセッションの開始

    <?php
    ...
    require_once 'Zend/Session.php';
    Zend_Session::start();
    ...
    ?>

起動ファイルでセッションを開始する際には、 ヘッダがブラウザに送信される前に確実にセッションが始まるようにします。 そうしないと例外が発生してしまい、おそらくユーザが見るページは崩れてしまうでしょう。 さまざまな高度な機能を使用するには、まず `Zend_Session::start()` が必要です (高度な機能の詳細については後で説明します)。

Zend_Session を使用してセッションを開始する方法は四通りありますが、 そのうち二つは間違った方法です。

- 1. 間違い: PHP の session.auto_start (http://www.php.net/manual/ja/ref.session.php#ini.session.auto-start) を、php.ini や .htaccess で設定してはいけません。 もし mod_php (やそれと同等のもの) を使用しており、 php.ini でこの設定が有効になっている、かつそれを無効にすることができない という場合は、.htaccess ファイル (通常は HTML のドキュメントルートにあります) に `php_value session.auto_start 0` を追加します。

  v

- 2. 間違い: PHP の `session_start()` 関数を直接使用してはいけません。 `session_start()` を直接使用した後で Zend_Session_Namespace を使用すると、 `Zend_Session::start()` が例外 ("session has already been started") をスローします。Zend_Session_Namespace を使用するか 明示的に `Zend_Session::start()` で開始した後で `session_start()` をコールすると、E_NOTICE が発生し、そのコールは無視されます。

- 3. 正解: `Zend_Session::start()` を使用します。 すべてのリクエストでセッションを使用したい場合は、 この関数コールを ZF の起動コードの最初のほうで無条件に記述します。 セッションにはある程度のオーバーヘッドがあります。 セッションを使用したいリクエストとそうでないリクエストがある場合は、

  - 起動コード内で、無条件にオプション `strict` を true にします ( `Zend_Session::setOptions()` を参照ください )。

  - セッションを必要とするリクエスト内で、 最初に `new Zend_Session_Namespace()` をコールする前に、`Zend_Session::start()` をコールします。

  - 通常どおり、必要に応じて `new Zend_Session_Namespace()` を使用します。事前に `Zend_Session::start()` がコールされていることを確認しておきましょう。

  `strict` オプションにより、`new Zend_Session_Namespace()` が自動的に `Zend_Session::start()` でセッションを開始することがなくなります。 したがって、このオプションを使用すると、ZF アプリケーションの開発者が 特定のリクエストにはセッションを使用しないという設計をおこなうことができます。 このオプションを使用すると、明示的に `Zend_Session::start()` をコールする前に Zend_Session_Namespace のインスタンスを作成しようとしたときに例外がスローされます。 ZF のコアライブラリのコードではこのオプションを使用しないでください。 このような設計上の決断をくだすのは、アプリケーションの開発者だからです。 同様に、"ライブラリ" の開発者も、`Zend_Session::setOptions()` の使用がユーザにどれだけの影響を与えるかを注意するようにしましょう。 これらのオプションは (もととなる ext/session のオプションと同様)、 全体に副作用を及ぼすからです。

- 4. 正解: 必要に応じて `new Zend_Session_Namespace()` を使用します。 セッションは、Zend_Session の内部で自動的に開始されます。 これはもっともシンプルな使用法で、たいていの場合にうまく動作します。 しかし、デフォルトであるクッキーベースのセッション (強く推奨します) を使用している場合には、PHP がクライアントに何らかの出力 (HTTP ヘッダ など) をする 前に、確実に 最初の `new Zend_Session_Namespace()` をコールしなければなりません。 出力バッファリング を使用すると、この問題を解決することができ、パフォーマンスも向上させることができるでしょう。 たとえば、`php.ini` で "`output_buffering = 65535`" とすると、出力バッファリング用のバッファが 64K になります。

### 25.3.2. セッション名前空間のロック

セッション名前空間をロックし、 それ以降その名前空間のデータに手を加えられないようにすることができます。 特定の名前空間を読み取り専用にするには Zend_Session_Namespace の `lock()` を、そして 読み取り専用の名前空間を読み書きできるようにするには `unLock()` を使用します。`isLocked()` を使用すると、 その名前空間がロックされているかどうかを調べることができます。 このロックは一時的なものであり、そのリクエスト内でのみ有効となります。 名前空間をロックしても、その名前空間に保存されているオブジェクトの セッターメソッドには何の影響も及ぼしません。 しかし、名前空間自体のセッターメソッドは使用できず、 名前空間に直接格納されたオブジェクトの削除や置換ができなくなります。同様に、 Zend_Session_Namespace の名前空間をロックしたとしても、 同じデータをさすシンボルテーブルの使用をとめることはできません (PHP のリファレンスについての説明も参照ください)。

例 25.7. セッション名前空間のロック

    <?php
        // このように仮定します
        $userProfileNamespace = new Zend_Session_Namespace('userProfileNamespace');

        // このセッションに読み取り専用ロックをかけます
        $userProfileNamespace->lock();

        // 読み取り専用ロックを解除します
        if ($userProfileNamespace->isLocked()) {
            $userProfileNamespace->unLock();
        }
    ?>

ウェブの世界で、MVC のモデルをどのように管理するかについては、 さまざまな考え方があります。その中のひとつに、 ビューで使用するプレゼンテーションモデルを作成するというものもあります。 ドメインモデルの中にある既存のデータで十分ということもあるでしょう。 ビューの中でこれらのデータに処理ロジックが書きくわえられてしまうことのないように、 セッション名前空間をロックしてからその「プレゼンテーション」 モデルをビューに渡すようにしましょう。

例 25.8. ビューにおけるセッションのロック

    <?php
    class FooModule_View extends Zend_View
    {
        public function show($name)
        {
            if (!isset($this->mySessionNamespace)) {
                $this->mySessionNamespace = Zend::registry('FooModule');
            }

            if ($this->mySessionNamespace->isLocked()) {
                return parent::render($name);
            }

            $this->mySessionNamespace->lock();
            $return = parent::render($name);
            $this->mySessionNamespace->unLock();

            return $return;
        }
    }
    ?>

### 25.3.3. コントローラでのセッションのカプセル化

名前空間を使用すると、コントローラによるセッションへのアクセスの際に 変数の汚染を防ぐこともできます。 たとえば、'Zend_Auth' コントローラでは、そのセッション状態データを 他のコントローラとは別に管理することになるでしょう。

例 25.9. コントローラでの名前空間つきセッションによる有効期限の管理

    <?php
    require_once 'Zend/Session.php';
    // 質問を表示するコントローラ
    $testSpace = new Zend_Session_Namespace('testSpace');
    $testSpace->setExpirationSeconds(300, "accept_answer"); // $test_session->setExpirationSeconds(300, "accept_answer"); // この変数にだけ有効期限を設定します
    $testSpace->accept_answer = true;

    --

    // 回答を処理するコントローラ
    $testSpace = new Zend_Session_Namespace('testSpace');

    if ($testSpace->accept_answer === true) {
        // 時間内
    }
    else {
        // 時間切れ
    }
    ?>

### 25.3.4. 名前空間内での Zend_Session_Namespace のインスタンスをひとつに制限する

ここで説明する機能を使用するよりも、セッションのロック (上を参照ください) を使うことを推奨します。ここで説明する機能は、 各名前空間へのアクセスが必要なすべての関数およびオブジェクトに Zend_Session_Namespace のインスタンスを渡さなければならず、 開発者への負担が大きくなります。

特定の名前空間用に Zend_Session_Namespace の最初のインスタンスを作成する際に、 その名前空間ではこれ以上別の Zend_Session_Namespace を作成しないよう指示することができます。 こうすると、その後同じ名前空間で Zend_Session_Namespace のインスタンスを作成しようとした際にエラーが発生します。 これはオプションの設定であり、デフォルトではありません。ひとつの名前空間に対しては ひとつのインスタンスだけを使用したいという人のために残しています。 これは、特定のセッション名前空間を コンポーネントが不意に書き換えてしまう危険性を減らします。 セッションへのアクセスが容易ではなくなるからです。 しかし、名前空間に対してひとつのインスタンスに限定してしまうと、 コードの量が増え、より複雑になってしまいます。なぜなら、便利な `$aNamespace = new Zend_Session_Namespace('aNamespace');` が最初の一度しか使えなくなるからです。それ以降は、以下の例のようになります。

例 25.10. 単一のインスタンスへの制限

    <?php
        require_once 'Zend/Session.php';
        $authSpaceAccessor1 = new Zend_Session_Namespace('Zend_Auth');
        $authSpaceAccessor2 = new Zend_Session_Namespace('Zend_Auth', Zend_Session_Namespace::SINGLE_INSTANCE);
        $authSpaceAccessor1->foo = 'bar';
        assert($authSpaceAccessor2->foo, 'bar'); // 通過します
        doSomething($options, $authSpaceAccessor2); // 必要に応じてアクセサを渡します
        .
        .
        .
        $aNamespaceObject = new Zend_Session_Namespace('Zend_Auth'); // これはエラーとなります
    ?>

上の例で Zend_Session_Namespace のコンストラクタの第二パラメータで指定しているのは、 今後 'Zend_Auth' 名前空間で新たに Zend_Session を作成することができないということです。 作成しようとすると、例外がスローされます。 上のコードを実行した後は `new Zend_Session_Namespace('Zend_Auth')` ができなくなります。そのため、 同一リクエスト内でその名前空間のセッションを使用するには、 最初に作成したインスタンス (上の例では `$authSpaceAccessor2`) をどこかに保存しておく必要があります。 たとえば静的変数にこのインスタンスを格納したり、 この名前空間のセッションを必要とするメソッドに直接渡したりします。 セッションのロック (上を参照ください) のほうが、 名前空間へのアクセスを制限する方法としてはより便利で簡単です。

### 25.3.5. 名前空間での配列の使用

名前空間内の配列を変更することはできません。 最も簡単な対応法は、必要な値をすべて設定してから配列を保存することです。 ZF-800 で、マジックメソッドと配列を使用している多くの PHP アプリケーションに影響する既知の問題を説明しています。

例 25.11. 配列に関する既知の問題

    <?php
        $sessionNamespace = new Zend_Session_Namespace('Foo');
        $sessionNamespace->array = array();
        $sessionNamespace->array['testKey'] = 1; // PHP 5.2.1 より前のバージョンでは動作しません
    ?>

セッション名前空間に代入した後で配列を変更する必要が出てきた場合は、 まずいったん配列を取得します。

例 25.12. 回避策: 取得して変更し、そして保存する

    <?php
        $sessionNamespace = new Zend_Session_Namespace('Foo');
        $sessionNamespace->array = array('tree' => 'apple');
        $tmp = $sessionNamespace->array;
        $tmp['fruit'] = 'peach';
        $sessionNamespace->array = $tmp;
    ?>

あるいは、目的の配列への参照を保持する配列をセッションに保存し、 間接的にアクセスします。

例 25.13. 回避策: 参照を含む配列を保存する

    <?php
        $myNamespace = new Zend_Session_Namespace('mySpace');

        // バグのあるバージョンの PHP でも動作します
        $a = array(1,2,3);
        $myNamespace->someArray = array( & $a ) ;
        $a['foo'] = 'bar';
    ?>

### 25.3.6. セッションと認証の共用

`Zend_Auth` 用の認証アダプタが返す認証結果がオブジェクトであって (非推奨です) 配列ではなかった場合は、セッションを開始する前に 認証クラスを require しておく必要があります。 そのかわりに、セッション名前空間の既知のキーをもとに認証アダプタ内で計算した値を 保存しておくことを推奨します。 たとえば、`Zend_Auth` のデフォルトの動作は、 これを名前空間 'Zend_Auth' のキー 'storage' に配置します。

`Zend_Auth` に対して認証トークンをセッション間で持続させないよう指示し、 手動で認証 ID をセッションに格納することもできます。 そうすれば、セッション名前空間内のよく知られた場所を使用できます。 アプリケーションによっては、使用する権限情報や認証情報を 特定の場所に保存しなければならないこともあるでしょう。 多くのアプリケーションは、認証時 つまり Zend_Auth の `authenticate()` メソッド実行時の ID (たとえばユーザ名) を特定の ID (一意に割り当てた整数値など) に関連付けています。

例 25.14. 例: 単純化した認証 ID へのアクセス

    <?php
        // 認証前のリクエスト
        require_once 'Zend/Auth/Result.php';
        require_once 'Zend/Auth/Adapter/Digest.php';
        $adapter = new Zend_Auth_Adapter_Digest($filename, $realm, $username, $password);
        $result = $adapter->authenticate();
        require_once 'Zend/Session/Namespace.php';
        $namespace = Zend_Session_Namespace('Zend_Auth');
        if ($result->isValid()) {
            $namespace->authorizationId = $result->getIdentity();
            $namespace->date = time();
        } else {
            $namespace->attempts++;
        }

        // それ以降のリクエスト
        require_once 'Zend/Session.php';
        Zend_Session::start();
        $namespace = Zend_Session_Namespace('Zend_Auth');

        echo "Valid: ", (empty($namespace->authorizationId) ? 'No' : 'Yes'), "\n"';
        echo "Authorization / user Id: ", (empty($namespace->authorizationId)
            ? 'none' : print_r($namespace->authorizationId, true)), "\n"';
        echo "Authentication attempts: ", (empty($namespace->attempts)
            ? '0' : $namespace->attempts), "\n"';
        echo "Authenticated on: ",
            (empty($namespace->date) ? 'No' : date(DATE_ATOM, $namespace->date), "\n"';
    ?>

### 25.3.7. ユニットテストでのセッションの使用

Zend Framework 自体のテストには PHPUnit を使用しています。 多くの開発者は、このテストスイートを拡張して自分のアプリケーションのコードをテストしています。 ユニットテスト中で、セッションの終了後に書き込み関連のメソッドを使用すると "Zend_Session is currently marked as read-only" という例外がスローされます。しかし、Zend_Session を使用するユニットテストには要注意です。 セッションを閉じたり (`Zend_Session::writeClose()`) 破棄したり (`Zend_Session::destroy()`) したら、 それ以降は Zend_Session_Namespace へのキーの設定や削除ができなくなります。 これは、ext/session や、PHP の PHP `session_destroy()` および `session_write_close()` の仕様によるものです, これらには、ユニットテストの setup/teardown 時に使用できるような、いわゆる "undo" 機能が備わっていないのです。

この問題の回避策は、 `tests/Zend/Session/SessionTest.php および SessionTestHelper.php` のユニットテストテスト `testSetExpirationSeconds()` を参照ください。 これは、PHP の `exec()` によって別プロセスを起動しています。 新しいプロセスが、ブラウザからの二番目以降のリクエストをシミュレートします。 この別プロセスの開始時にはセッションを "初期化" します。 ちょうど、ふつうの PHP スクリプトがウェブリクエストを実行する場合と同じような動作です。 また、呼び出し元のプロセスで `$_SESSION[]` を変更すると、 子プロセスでそれが反映されます。親側では `exec()` を使用する前にセッションを閉じています。

例 25.15. PHPUnit による、Zend_Session* を使用したコードのテスト

    <?php
            // setExpirationSeconds() をテストします
            require 'tests/Zend/Session/SessionTestHelper.php'; // trunk/ の SessionTest.php も参照ください
            $script = 'SessionTestHelper.php';
            $s = new Zend_Session_Namespace('space');
            $s->a = 'apple';
            $s->o = 'orange';
            $s->setExpirationSeconds(5);

            Zend_Session::regenerateId();
            $id = Zend_Session::getId();
            session_write_close(); // セッションを開放し、これ以降で使用できるようにします
            sleep(4); // 無効となるほどの時間ではありません
            exec($script . "expireAll $id expireAll", $result);
            $result = $this->sortResult($result);
            $expect = ';a === apple;o === orange;p === pear';
            $this->assertTrue($result === $expect,
                "iteration over default Zend_Session namespace failed; expecting result === '$expect', but got '$result'");

            sleep(2); // 無効になります (全部で 6 秒待機していますが、有効期限は 5 秒です)
            exec($script . "expireAll $id expireAll", $result);
            $result = array_pop($result);
            $this->assertTrue($result === '',
                "iteration over default Zend_Session namespace failed; expecting result === '', but got '$result')");
            session_start(); // 人為的にサスペンドしたセッションを復活させます

            // これを別のテストに分割するることもできます。しかし実際のところ、
            // 上のテストの残骸が以下のテストに影響を及ぼすとしたら、それはバグでしょう。
            // バグは、ここで発見しておくべきものです。
            $s = new Zend_Session_Namespace('expireGuava');
            $s->setExpirationSeconds(5, 'g'); // 名前空間内のキーひとつだけを無効にしようとします
            $s->g = 'guava';
            $s->p = 'peach';
            $s->p = 'plum';

            session_write_close(); // セッションを開放し、これ以降で使用できるようにします
            sleep(6); // 無効となるほどの時間ではありません
            exec($script . "expireAll $id expireGuava", $result);
            $result = $this->sortResult($result);
            session_start(); // 人為的にサスペンドしたセッションを復活させます
            $this->assertTrue($result === ';p === plum',
                "iteration over named Zend_Session namespace failed (result=$result)");
    ?>

前のページ | 上に戻る | 次のページ
25.2. 基本的な使用法 | ホーム | 25.4. グローバルセッションの管理