13日目: ユーザー
昨日の内容にはたくさんの情報が詰め込まれていました。ごくわずかな PHP コードと symfony アドミンジェネレータによって、短時間でバックエンドインターフェイスを作ることができます。
今日は、HTTP リクエストの間の永続データを管理する方法を理解します。ご存じの通り、HTTP プロトコルはステートレスです。これはそれぞれのリクエストはその前後のリクエストから独立していることを意味します。現代の Web サイトはユーザーエクスペリエンスを強化するためにリクエストの間のデータを永続化する方法が必要です。
ユーザーセッションは Cookie を利用して識別できます。symfony において、開発者はセッションを直接操作する必要はありませんが、アプリケーションを使うエンドユーザーを表す sfUser
オブジェクトを使うことが必要です。
ユーザーフラッシュ
フラッシュを表示するアクションでユーザーオブジェクトをすでに見てきました。フラッシュ (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.', $job->getExpiresAt('m/d/Y'))); $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));
}
最初の引数はフラッシュの識別子で、2番目は表示するメッセージです。望めばどんなフラッシュ定義できますが、notice
と error
はもっとも良く使われるものの2つです (これらはアドミンジェネレータが広範囲で使います)。
テンプレートにフラッシュメッセージを含めるのは開発者しだいですが、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
を通してアクセス可能です.
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); } // ... }
JobeetJob
オブジェクトをセッションに直接保存することは可能です。しかしセッションに直接保存することは推奨できません。なぜならセッション変数はリクエスト間でシリアライズされるからです。セッションがロードされ、それらが修正もしくは削除される場合、JobeetJob
オブジェクトはデシリアライズされて「盗まれる」恐れがあります。
getAttribute()
、setAttribute()
識別子を与える sfUser::getAttribute()
メソッドはユーザーセッションから値を取得します。逆に言えば、識別子のために setAttribute()
メソッドはどんな PHP 変数もセッションに保存します。
getAttribute()
メソッドは識別子がまだ定義されていない場合に返すデフォルト値のオプションも取ります。
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());
return JobeetJobPeer::retrieveByPKs($ids);
}
public function getJobHistory() { $ids = $this->getAttribute('job_history', array());
if (!empty($ids))
{
return Doctrine_Core::getTable('JobeetJob')
->createQuery('a')
->whereIn('a.id', $ids)
->execute()
;
}
return array();
}
// ...
}
getJobHistory()
メソッドは 1 回の呼び出しで複数の JobeetJob
オブジェクトを取得するために、 Propel の retrieveByPKs()
メソッドを使います。 getJobHistory()
メソッドは 1 回の呼び出しで複数の JobeetJob
オブジェクトを取得するために、 カスタム Doctrine_Query オブジェクトを使います。
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
のプロキシメソッドが無いので、パラメータホルダオブジェクトを直接使う必要があります。
sfParameterHolder
クラスはパラメータを保存するためsfRequest
によっても使われます。
アプリケーションのセキュリティ
認証
他の多くの symfony の機能のように、セキュリティは YAML ファイルの security.yml
で管理されます。たとえば、デフォルトコンフィギュレーションはバックエンドアプリケーションの config/
ディレクトリで見つかります:
---
# apps/backend/config/security.yml
default:
is_secure: false
is_secure
エントリを true
に切り替えると、バックエンドアプリケーション全体でユーザーを認証することが求められます。
YAML ファイルにおいて、ブール値は
true
とfalse
の文字列で表わされます。
Web デバッグツールバーのログを見ると、ページにアクセスしようとするたびに defaultActions
クラスの executeLogin()
メソッドが呼び出されることに気が付くでしょう。
認証されていないユーザーがセキュアなアクションにアクセスしようとすると、symfony は settings.yml
で設定される login
アクションにリクエストを転送します:
---
all:
.actions:
login_module: default
login_action: login
無限ループを回避するため、 login アクションをセキュアにすることはできません。
4日目で見たように、同じ設定ファイルを複数の場所で定義できます。これは
security.yml
にもあてはまります。単独のアクションもしくはモジュール全体をセキュアにするかしないかのみであれば、モジュールのconfig/
ディレクトリでsecurity.yml
を作ります:--- index: is_secure: false all: is_secure: true
デフォルトで、myUser
クラスは sfUser
ではなく、 sfBasicSecurityUser
を継承します。sfBasicSecurityUser
はユーザーの認証と認可を管理するための追加メソッドを提供します。 ユーザー認証を管理するために、isAuthenticated()
と setAuthenticated()
メソッドを使います:
if (!$this->getUser()->isAuthenticated()) { $this->getUser()->setAuthenticated(true); }
認証
ユーザーが認証されるとき、アクションへのアクセスはクレデンシャル (資格) を定義することでより制限できます。ユーザーはページにアクセスするために要求されるクレデンシャルがなければなりません:
---
default:
is_secure: false
credentials: admin
symfony のクレデンシャルシステムはとてもシンプルで強力です。クレデンシャルは (グループもしくはパーミッションのように) アプリケーションのセキュリティモデルを記述するために必要なものを表現できます。
複雑なクレデンシャル
複雑なクレデンシャルの要件を記述するために
security.yml
のcredentials
エントリはブール値オペレーションをサポートします。ユーザーがクレデンシャル A と B をもたなければならない場合、クレデンシャルを角かっこで囲みます:
--- index: credentials: [A, B]
ユーザーがクレデンシャル A もしくは B をもたなければならない場合、クレデンシャルを2つの角かっこの組で囲みます:
--- index: credentials: [[A, B]]
いくつのクレデンシャルでも任意に要否を記述するためにかっこを組み合わせることもできます。
ユーザークレデンシャルを管理するために、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つ: admin しかないのでクレデンシャルは使いません。
プラグイン
車輪の再発明をしたくないので、スクラッチでログインアクションを開発しません。代わりに、プラグインをインストールします。
symfony フレークワークの大きな強みの1つはプラグインエコシステムです。来る日に行いますが、プラグインを作るのはとても簡単です。プラグインはコンフィギュレーションからモジュールとアセットまで何でも格納できるのでとても強力です。
今日は、バックエンドアプリケーションをセキュアにするために ~sfGuardPlugin
~ をインストールします:
$ php symfony plugin:install sfGuardPlugin
今日は、バックエンドアプリケーションをセキュアにするために sfDoctrineGuardPlugin
をインストールします。
$ php symfony plugin:install sfDoctrineGuardPlugin
plugin:install
タスクは名前からプラグインをインストールします。すべてのプラグインは plugins/
ディレクトリの下に保存され、それぞれのプラグインはプラグインの名前による専用のディレクトリを持ちます。
plugin:install
タスクを動くようにするために PEAR をインストールしなければなりません。
plugin:install
タスクでプラグインをインストールするとき、symfony は最新の安定版をインストールします。プラグインの特定バージョンをインストールするには、--release
オプションを渡します。
プラグインページは symfony のバージョンによってグループにまとめられるすべてのバージョンの一覧を示します。
プラグインはディレクトリで自己展開しますが、symfony 公式サイトからパッケージをダウンロードして展開する、もしくは代わりに Subversion リポジトリへの svn:externals
リンクを作ります。
プラグインページ は symfony のバージョンでグループ化された利用可能なすべてのバージョンの一覧を表示します。
プラグインはディレクトリに格納されており、symfony 公式サイトからパッケージをダウンロードして展開することも可能で、もしくは代わりに Subversion リポジトリへの svn:externals
リンクからダウンロードできます。
plugin:install
タスクは ProjectConfiguration.class.php
ファイルを自動的に更新することでインストールするプラグインを自動的に有効にします。しかし Subversion を通して、またはアーカイブをダウンロードしてプラグインをインストールする場合、ProjectConfiguration.class.php
で手動で有効にする必要があります:
// config/ProjectConfiguration.class.php class ProjectConfiguration extends sfProjectConfiguration { public function setup() {
$this->enablePlugins(array('sfPropelPlugin', 'sfGuardPlugin')); $this->enablePlugins(array( 'sfDoctrinePlugin', 'sfDoctrineGuardPlugin' )); } }
バックエンドのセキュリティ
それぞれのプラグインには設定方法を説明しているREADMEREADMEファイルがあります。
新しいプラグインの設定方法を見てみましょう。ユーザー、グループとパーミッションを管理する新しいモデルクラスを提供するので、モデルをリビルドする必要があります:
$ php symfony propel:build --all --and-load --no-confirmation $ php symfony doctrine:build --all --and-load --no-confirmation
propel:build --all --and-load
doctrine:build --all --and-load
タスクは既存のすべてのテーブルを再生成する前にこれらを削除することを覚えておいてください。これを避けるには、モデル、フォーム、フィルタをビルドしてから、data/sql/
に保存されている、生成された SQL 文を実行して新しいテーブルを作成します。
sfGuardPlugin
はユーザークラスにいくつかのメソッドを追加するので、myUser
の基底クラスを sfGuardSecurityUser
に変更する必要があります: sfDoctrineGuardPlugin
はユーザークラスにいくつかのメソッドを追加するので、myUser
の基底クラスをsfGuardSecurityUser
に変更する必要があります:
// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser { }
sfGuardPlugin
はユーザーを認証する signin
アクションを sfGuardAuth
モジュールに提供します。 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
設定に追加することで使いたいモジュールを明示的に有効にする必要があります。
最後のステップは admin ユーザーを作ることです:
$ php symfony guard:create-user fabien SecretPass
$ php symfony guard:promote fabien
Subversion のトランクから
sfDoctrineGuardPlugin
をインストールしたのであれば、ユーザーを作って昇格させることを一度に行うために次のコマンドを実行する必要があります:$ php symfony guard:create-user [email protected] fabien SecretPass Fabien Potencier
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') ?></li> <li><?php echo link_to('Categories', 'jobeet_category') ?></li> </ul> </div> <?php endif ?>
ユーザーが認証されたとき、メニューにログアウトリンクを追加する必要があります:
// apps/backend/templates/layout.php <li><?php echo link_to('Logout', 'sf_guard_signout') ?></li>
sfGuardPlugin
によって提供されるすべてのルートの一覧を表示するには、app:routes
タスクを使います。
sfDoctrineGuardPlugin
から提供されるすべてのルートの一覧を表示するには、app:routes
タスクを使います。
Jobeet バックエンドにさらに磨きをかけるために、管理者ユーザーを管理する新しいモジュールを追加しましょう。ありがたいことに、sfGuardPlugin
はそのようなモジュールを提供してくれます。sfGuardAuth
モジュールに関して、settings.yml
でこれを有効にする必要があります:
リンクをメニューに追加します:
// apps/backend/templates/layout.php <li><?php echo link_to('Users', 'sf_guard_user') ?></li>
完了です!
ユーザーのテスト
ユーザーのテストの話をしていないので13日目は終わっていません。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()
メソッドは渡されたユーザー属性をチェックします。
sfTesterUser
テスターはユーザーの認証と資格をテストするためにisAuthenticated()
とhasCredential()
メソッドも提供します。
また明日
symfony のユーザークラスは PHP セッションの管理を抽象化するためのよい手段です。symfony の素晴らしいなプラグインシステムと sfGuardPlugin
プラグインを結びつけることで短時間で Jobeet バックエンドをセキュアにすることができました。またプラグインによって提供されたモジュールのおかげで、自由に管理者ユーザーを管理できるクリーンなインターフェイスも追加しました。
ORM
インデックス
Document Index
関連ページリスト
Related Pages
- 1日目: プロジェクトを始める
- 2日目: プロジェクト
- 3日目: ~データモデル~
- 4日目: Controller と View
- 5日目: ルーティング
- 6日目: モデルの詳細
- 7日目: カテゴリページで遊ぶ
- 8日目: ユニットテスト
- 9日: 機能テスト
- 10日目: フォーム
- 11日目: フォームをテストする
- 12日目: アドミンジェネレータ
- 13日目: ユーザー
- 14日目: フィード
- 15日目: Web サービス
- 16日目: ~メーラー~
- 17日目: 検索
- 18日目: ~AJAX~
- 19日目: 国際化とローカライゼーション
- 20日目: プラグイン
- 21日目: キャッシュ
- 22日目: デプロイ
- 23日目: 別の視点から symfony を見る
- Appendix A - License
- 謝辞
日本語ドキュメント
Japanese Documents
- 2011/01/18 Chapter 17 - Extending Symfony
- 2011/01/18 The generator.yml Configuration File
- 2011/01/18 Les tâches
- 2011/01/18 Emails
- 2010/11/26 blogチュートリアル(8) ビューの作成
リリース情報
Release Information
- 2.0 : 2.0.15(2011/05/30)
Symfony2日本語ドキュメント - 1.4 : 1.4.18(2012/05/30)
Changelog