昨日はたくさんの情報が詰め込まれました。 ごくわずかなPHPコードとadminジェネレーターによって、短時間でバックエンドインターフェイスを作成できます。
今日は、HTTPリクエストの間の永続データを管理する方法を理解します。 ご存じのとおり、HTTPプロトコルはステートレスです。 それぞれのリクエストはその前後のリクエストから独立していることを意味します。 現代のWebサイトはユーザーエクスペリエンスを強化するためにリクエストの間のデータを一貫させる方法が必要です。
ユーザーセッションはCookieを利用して特定できます。
symfonyにおいて、開発者はセッションを直接操作する必要はありませんがむしろアプリケーションのエンドユーザーを表すsfUser
オブジェクトを使うことが必要です。
ユーザーflash
flashつきのアクションでユーザーオブジェクトをすでに見てきました。 flashはユーザーセッションに保存される短期のメッセージです。 これはすぐ次のリクエストの後で自動的に削除されます。 リダイレクトした後でユーザーにメッセージを表示する必要があるときにとても役立ちます。 jobが保存される、削除もしくは延長されるときに、ユーザーにフィードバックを表示するためにadminジェネレーターはflashをたくさん使います。
flashはsfUser
のsetFlash()
メソッドを使って設定できます:
// apps/frontend/modules/job/actions/actions.class.php public function executeExtend(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $this->forward404Unless($job->extend()); $this->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', date('m/d/Y', strtotime($job->getExpiresAt())))); $this->redirect($this->generateUrl('job_show_user', $job)); }
最初の引数はflashの識別子で2番目は表示するメッセージです。
望むのであればflashはなんでも定義できますが、notice
とerror
はもっとも共通するものの2つです
(これらはadminジェネレーターによって広範囲で使われます)。
テンプレートにflashメッセージをインクルードするのは開発者しだいですが、Jobeetに関して、これらはlayout.php
によって出力されます:
// apps/frontend/templates/layout.php <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div> <?php endif; ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div> <?php endif; ?>
テンプレートにおいて、ユーザーは特別な変数sf_user
をとおしてアクセス可能です.
note
symfonyオブジェクトの中にはアクションから明示的に渡さなくても常にテンプレートにアクセスできるものがあります: >sf_request
、sf_user
とsf_response
ユーザー属性
不幸なことに、Jobeetユーザーのストーリーにはユーザーセッションに何かを保存する要件は含まれていません。 ですので新しい要件を追加しましょう: 求人の閲覧を楽にするために、ユーザーによって閲覧される最新の3件の求人は後で求人ページに戻れるリンクつきのメニューに表示されます。
ユーザーが求人ページにアクセスするとき、表示されるjobオブジェクトをユーザーの履歴に追加してセッションに保存する必要があります:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); // 求人履歴にすでに保存された求人を取得する $jobs = $this->getUser()->getAttribute('job_history', array()); // 配列の始めに現在の求人を追加する array_unshift($jobs, $this->job->getId()); // 新しい求人履歴をセッションに保存し直す $this->getUser()->setAttribute('job_history', $jobs); } // ... }
note
JobeetJob
オブジェクトをセッションに直接保存するのはできますが非推奨です。
リクエストの間にセッション変数がシリアライズされるからです。
セッションがロードされるとき、JobeetJob
オブジェクトはデシリアライズされその間にそれらが修正もしくは削除される場合に"盗まれます"。
getAttribute()
、setAttribute()
識別子として、sfUser::getAttribute()
メソッドはユーザーセッションから値を取得します。
逆に言えば、識別子のためにsetAttribute()
メソッドはPHP変数をセッションに保存します。
getAttribute()
メソッドは識別子がまだ定義されていない場合に返すオプションのデフォルト値も受け取ります。
note
getAttribute()
メソッドが受け取るデフォルトの値は次の内容のショートカットです:
if (!$value = $this->getAttribute('job_history')) { $value = array(); }
myUser
クラス
関心の分離をより順守するために、コードをmyUser
クラスに移動させましょう。
myUser
クラスはデフォルトのsfUser
基底クラスをアプリケーション固有のふるまいでオーバーライドします:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); $this->getUser()->addJobToHistory($this->job); } // ... } // apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function addJobToHistory(JobeetJob $job) { $ids = $this->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $this->setAttribute('job_history', array_slice($ids, 0, 3)); } } }
すべての要件を考慮するようにコードも変更されました:
!in_array($job->getId(), $ids)
: 求人を重複して履歴に保存できないarray_slice($ids, 0, 3)
: ユーザーによって閲覧される最新の3つの求人のみ表示される
レイアウトにおいて、変数$sf_content
が出力される前に次のコードを追加します:
// apps/frontend/templates/layout.php <div id="job_history"> Recent viewed jobs: <ul> <?php foreach ($sf_user->getJobHistory() as $job): ?> <li> <?php echo link_to($job->getPosition().' - '.$job->getCompany(), 'job_show_user', $job) ?> </li> <?php endforeach; ?> </ul> </div> <div class="content"> <?php echo $sf_content ?> </div>
レイアウトは現在の求人履歴を読み取るために新しいgetJobHistory()
メソッドを使います:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function getJobHistory() { $ids = $this->getAttribute('job_history', array()); if (!empty($ids)) { return Doctrine::getTable('JobeetJob') ->createQuery('a') ->whereIn('a.id', $ids) ->execute(); } else { return array(); } } // ... }
sfParameterHolder
求人履歴のAPIを完結させるために、履歴をリセットするメソッドを追加しましょう:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function resetJobHistory() { $this->getAttributeHolder()->remove('job_history'); } // ... }
ユーザー属性はsfParameterHolder
クラスのオブジェクトによって管理されます。
getAttribute()
とsetAttribute()
メソッドはgetParameterHolder()->get()
とgetParameterHolder()->set()
用のプロキシメソッドです。
remove()
メソッドはsfUser
のプロキシメソッドを持たないので、パラメーターホルダーオブジェクトを直接使う必要があります。
note
sfParameterHolder
クラスはパラメーターを保存するためにsfRequest
によっても使われます。
アプリケーションのセキュリティ
認証
他の多くのsymfonyの機能のように、セキュリティはYAMLファイルのsecurity.yml
で管理されます。
たとえば、デフォルトコンフィギュレーションはバックエンドアプリケーションのconfig/
ディレクトリで見つかります:
# apps/backend/config/security.yml default: is_secure: off
is_secure
エントリーをon
に切り替える場合、バックエンドアプリケーション全体でユーザーを認証することが求められます。
tip
YAMLファイルにおいて、booleanはtrue
とfalse
、もしくはon
とoff
の文字列で表現されます。
Webデバッグツールバーのログを見ると、ページにアクセスしようとするたびにdefaultActions
クラスのexecuteLogin()
メソッドが呼び出されることに気がつきます。
認証されていないユーザーがセキュアなアクションにアクセスしようとすると、symfonyはsettings.yml
で設定されるlogin
アクションにリクエストを転送します:
all: .actions: login_module: default login_action: login
note
無限の再帰呼び出しを回避するためにloginアクションをセキュアにするのは不可能です。
tip
4日目でみたように、同じ設定ファイルを複数の場所で定義できます。
これはsecurity.yml
にもあてはまります。
単独のアクションもしくはモジュール全体をセキュアにするもしくはセキュアにしないのみであれば、モジュールのconfig/
ディレクトリでsecurity.yml
を作成します:
index: is_secure: off all: is_secure: on
デフォルトでは、myUser
クラスはsfBasicSecurityUser
を継承し、sfUser
は継承しません。
sfBasicSecurityUser
はユーザーの認証と認可を管理するための追加メソッドを提供します。
ユーザーの認証を管理するには、isAuthenticated()
とsetAuthenticated()
メソッドを使います:
if (!$this->getUser()->isAuthenticated()) { $this->getUser()->setAuthenticated(true); }
認証
ユーザーが認証されたとき、アクションへのアクセスはクレデンシャル(credential)を定義することでより制限できます。 ユーザーはページにアクセスするために要求されるクレデンシャルを持たなければなりません:
default: is_secure: off credentials: admin
symfonyのクレデンシャルシステムはとてもシンプルで強力です。 クレデンシャルは(グループもしくはパーミッションのように)アプリケーションのセキュリティモデルを記述するために必要なものを表現できます。
ユーザークレデンシャルを管理するために、sfBasicSecurityUser
はいくつかのメソッドを提供します:
// 1つもしくは複数のクレデンシャルを追加する $user->addCredential('foo'); $user->addCredentials('foo', 'bar'); // ユーザーがクレデンシャルを持つかチェックする echo $user->hasCredential('foo'); => true // ユーザーが両方のクレデンシャルを持つかチェックする echo $user->hasCredential(array('foo', 'bar')); => true // ユーザーがクレデンシャルの1つを持つかチェックする echo $user->hasCredential(array('foo', 'bar'), false); => true // クレデンシャルを削除する $user->removeCredential('foo'); echo $user->hasCredential('foo'); => false // すべてのクレデンシャルを削除する(ログアウト処理の際に便利) $user->clearCredentials(); echo $user->hasCredential('bar'); => false
Jobeetバックエンドに関して、プロファイルは1つ: 管理者しかないのでクレデンシャルは使いません。
プラグイン
車輪の再発明をしたくないので、1からログインアクションを開発しません。 代わりに、プラグインをインストールします。
symfonyフレークワークの大きな強みの1つはプラグインのエコシステムです。 来たる日に見ますが、プラグインを作るのはとても簡単です。 プラグインはコンフィギュレーションからモジュールとアセットまで任意のものを格納できるのでとても強力です。
今日は、バックエンドアプリケーションをセキュアにするためにsfDoctrineGuardPlugin
をインストールします
$ php symfony plugin:install sfDoctrineGuardPlugin
plugin:install
タスクは名前でプラグインをインストールします。
すべてのプラグインはplugins/
ディレクトリの下に保存されそれぞれのプラグインはプラグインの名前から名づけた独自のディレクトリを持ちます。
note
plugin:install
タスクを動作させるにはPEARをインストールしなければなりません。
plugin:install
タスクでプラグインをインストールするとき、symfonyは最新の安定版をインストールします。
プラグインの特定バージョンをインストールするには、--release
オプションを渡します。
プラグインページ はsymfonyのバージョンでグループ化された利用可能なすべてのバージョンの一覧を表示します。
プラグインはディレクトリに内蔵され、symfony公式サイトからパッケージをダウンロードして展開することが可能で、代わりにSubversionリポジトリへのsvn:externals
プロパティを作成します。
tip
config/ProjectConfiguration.class.php
クラスのenableAllPluginsExcept()
メソッドを使わない場合、プラグインをインストールした後でかならずプラグインを有効にすることを覚えておいてください。
バックエンドのセキュリティ
それぞれのプラグインには設定方法を説明しているREADMEファイルが含まれます。
新しいプラグインの作り方を見てみましょう。 ユーザー、グループとパーミッションを管理する新しいモデルクラスを提供するので、モデルをリビルドする必要があります:
$ php symfony doctrine:build-all-reload
tip
doctrine:build-all-reload
タスクは既存のすべてのテーブルを再生成する前にこれらを削除することを覚えておいてください。
これを避けるには、モデル、フォーム、フィルターをビルドし、data/sql/
に保存されている生成SQLステートメントを実行して新しいテーブルを作成します。
新しいクラスを作るときは、常にsymfonyのキャッシュをクリアする必要があります:
$ php symfony cc
sfDoctrineGuardPlugin
はユーザークラスにいくつかのメソッドを追加するので、myUser
の基底クラスをsfGuardSecurityUser
に変更する必要があります:
// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser { }
sfDoctrineGuardPlugin
はユーザーを認証するsignin
アクションをsfGuardAuth
モジュールに提供します。
ログインページに使われるデフォルトのアクションを変更するためにsettings.yml
ファイルを編集します:
# apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth] # ... .actions: login_module: sfGuardAuth login_action: signin # ...
プラグインは1つのプロジェクトのすべてのアプリケーションで共有されるので、モジュールをenabled_modules
設定|enabled_modules
(設定)に追加することで使いたいモジュールを明示的に有効にする必要があります。
最後のステップは管理者ユーザーを作成することです:
$ php symfony guard:create-user fabien SecretPass $ php symfony guard:promote fabien
tip
sfGuardPlugin
はコマンドラインからユーザー、グループとパーミッションを管理するタスクを提供します。
guard
名前空間に所属するすべてのタスクの一覧を表示するにはlist
タスクを使います:
$ php symfony list guard
ユーザーが認証されていないとき、メニューバーを隠す必要があります:
// apps/backend/templates/layout.php <?php if ($sf_user->isAuthenticated()): ?> <div id="menu"> <ul> <li><?php echo link_to('Jobs', '@jobeet_job_job') ?></li> <li><?php echo link_to('Categories', '@jobeet_category_category') ?></li> </ul> </div> <?php endif; ?>
ユーザーが認証されたとき、メニューにログアウトリンクを追加する必要があります:
// apps/backend/templates/layout.php <li><?php echo link_to('Logout', '@sf_guard_signout') ?></li>
tip
sfGuardPlugin
によって提供されるすべてのルートの一覧を表示するには、app:routes
タスクを使います。
Jobeetバックエンドにさらに磨きをかけるには、管理者ユーザーを管理する新しいモジュールを追加しましょう。
ありがたいことに、sfGuardPlugin
はそのようなモジュールを提供してくれます。
sfGuardAuth
モジュールに関して、settings.yml
でこれを有効にする必要があります:
// apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth, sfGuardUser]
メニューにリンクを追加します:
// apps/backend/templates/layout.php <li><?php echo link_to('Users', '@sf_guard_user') ?></li>
やりました!
ユーザーのテスト
ユーザーのテストの話をしていないので今日のチュートリアルは終わっていません。
symfonyブラウザーはCookieをシミュレートするので、組み込みのsfTesterUser
テスターによって使われるユーザーのふるまいをテストするのはとても簡単です。
今日追加したメニュー機能用の機能テストを更新しましょう。
job
モジュールの機能テストの末尾に次のコードを追加します:
// test/functional/frontend/jobActionsTest.php $browser-> info('4 - User job history')-> loadData()-> restart()-> info(' 4.1 - When the user access a job, it is added to its history')-> get('/')-> click('Web Developer', array(), array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))-> end()-> info(' 4.2 - A job is not added twice in the history')-> click('Web Developer', array(), array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))-> end() ;
テスト作業を楽にするには、フィクスチャデータをリロードしてクリーンなセッションで始めるためにブラウザーを再起動します。
isAttribute()
メソッドは渡されたユーザー属性をチェックします。
note
sfTesterUser
テスターはユーザーの認証と認可をテストするためにisAuthenticated()
とhasCredential()
メソッドも提供します。
また明日
symfonyのユーザークラスはPHPセッションの管理を抽象化するためのよい手段です。
symfonyの偉大なプラグインシステムとsfGuardPlugin
プラグインを結びつけることで短時間でJobeetバックエンドをセキュアにすることができました。
またプラグインによって提供されたモジュールのおかげで、自由に管理者ユーザーを管理できるクリーンなインターフェイスも追加しました。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.