13日目: ユーザー
昨日の内容にはたくさんの情報が詰め込まれていました。ごくわずかな PHP コードと symfony アドミンジェネレータによって、短時間でバックエンドインターフェイスを作ることができます。
今日は、HTTP リクエストの間の永続データを管理する方法を理解します。ご存じの通り、HTTP プロトコルはステートレスです。これはそれぞれのリクエストはその前後のリクエストから独立していることを意味します。現代の Web サイトはユーザーエクスペリエンスを強化するためにリクエストの間のデータを永続化する方法が必要です。
ユーザーセッションは Cookie を利用して識別できます。symfony において、開発者はセッションを直接操作する必要はありませんが、アプリケーションを使うエンドユーザーを表す sfUser
オブジェクトを使うことが必要です。
ユーザーフラッシュ
フラッシュを表示するアクションでユーザーオブジェクトをすでに見てきました。フラッシュ (flash) はユーザーセッションに保存され、すぐ次のリクエストの後で自動的に削除される一時的なメッセージです。リダイレクトした後でユーザーにメッセージを表示する必要があるときにとても役立ちます。アドミンジェネレータは求人が保存、削除もしくは延長されるときに、ユーザーにフィードバックを表示するためにフラッシュを良く用います。
フラッシュは sfUser
の setFlash()
メソッドを使って設定できます:
[php]
// apps/frontend/modules/job/actions/actions.class.php
public function executeExtend(sfWebRequest $request)
{
$request->checkCSRFProtection();
$job = $this->getRoute()->getObject();
$this->forward404Unless($job->extend());
$this->redirect($this->generateUrl('job_show_user', $job));
}
最初の引数はフラッシュの識別子で、2番目は表示するメッセージです。望めばどんなフラッシュ定義できますが、notice
と error
はもっとも良く使われるものの2つです (これらはアドミンジェネレータが広範囲で使います)。
テンプレートにフラッシュメッセージを含めるのは開発者しだいですが、Jobeet の場合、これらは layout.php
によって出力されます:
[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 オブジェクトをユーザーの履歴に追加してセッションに保存する必要があります:
[php]
// 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()
メソッドが受け取るデフォルトの値は次の内容のショートカットです:[php] if (!$value = $this->getAttribute('job_history')) { $value = array(); }
myUser
クラス
関連事の分離をより順守するために、コードを myUser
クラスに移動させましょう。myUser
クラスはデフォルトの sfUser
基底クラスをアプリケーション固有のふるまいでオーバーライドします:
[php]
// 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
の出力の前に次のコードを追加します。
[php]
// 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()
メソッドを使います。
[php]
// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
return JobeetJobPeer::retrieveByPKs($ids);
}
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 を完全にするために、履歴をリセットするメソッドを追加しましょう。
[php]
// 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
によっても使われます。
アプリケーションのセキュリティ
認証
ほかの多くの機能と同じように、セキュリティは YAML ファイルの security.yml
ファイルで管理されます。たとえば、デフォルトのコンフィギュレーションはバックエンドアプリケーションの config/
ディレクトリで見つかります。
[yml]
# apps/backend/config/security.yml
default:
is_secure: false
is_secure
エントリを true
に切り替えると、バックエンドアプリケーション全体でユーザーを認証することが求められます。
TIP YAML ファイルにおいて、ブール値は
true
とfalse
の文字列で表わされます。
Web デバッグツールバーのログを見ると、ページにアクセスしようとするたびに defaultActions
クラスの executeLogin()
メソッドが呼び出されることがわかります。
認証されていないユーザーがセキュアなアクションにアクセスしようとすると、symfony は settings.yml
ファイルで設定される login
アクションにリクエストを転送します。
[yml]
all:
.actions:
login_module: default
login_action: login
NOTE 無限ループを回避するため、 login アクションをセキュアにすることはできません。
-
TIP 4日目で見たように、同じ設定ファイルを複数の場所で定義できます。これは
security.yml
ファイルにもあてはまります。単独のアクションもしくはモジュール全体をセキュアにするかしないかのみであれば、モジュールのconfig/
ディレクトリでsecurity.yml
ファイルを作ります。[yml] index: is_secure: false all: is_secure: true
デフォルトで、myUser
クラスは sfUser
ではなく、 sfBasicSecurityUser
を継承します。sfBasicSecurityUser
はユーザーの認証と認可を管理するための追加メソッドを提供します。
ユーザー認証を管理するために、isAuthenticated()
と setAuthenticated()
メソッドを使います。
[php]
if (!$this->getUser()->isAuthenticated())
{
$this->getUser()->setAuthenticated(true);
}
認証
ユーザーが認証されるとき、アクションへのアクセスはクレデンシャル (資格) を定義することでより制限できます。ユーザーはページにアクセスするために要求されるクレデンシャルがなければなりません。
[yml]
default:
is_secure: false
credentials: admin
symfony のクレデンシャルシステムはとてもシンプルで強力です。クレデンシャルは (グループもしくはパーミッションのように) アプリケーションのセキュリティモデルを記述するために必要なものを表現できます。
SIDEBAR 複雑なクレデンシャル
複雑なクレデンシャルの要件を記述するために
security.yml
ファイルのcredentials
エントリはブール値の演算をサポートします。ユーザーがクレデンシャル A と B をもたなければならない場合、クレデンシャルを角かっこで囲みます。
[yml] index: credentials: [A, B]
ユーザーがクレデンシャル A もしくは B をもたなければならない場合、クレデンシャルを2つの角かっこの組で囲みます。
[yml] index: credentials: [[A, B]]
いくつのクレデンシャルでも任意に要否を記述するためにかっこを組み合わせることもできます。
ユーザークレデンシャルを管理するために、sfBasicSecurityUser
はいくつかのメソッドを提供します。
[php]
// 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/
ディレクトリの下に保存され、それぞれのプラグインにはプラグインの名前による専用のディレクトリが用意されています。
NOTE
plugin:install
タスクを動くようにするために PEAR をインストールしなければなりません。
plugin:install
タスクでプラグインをインストールするとき、symfony は最新の安定版をインストールします。プラグインの特定バージョンをインストールするには、--release
オプションを渡します。
プラグインはディレクトリで自己展開しますが、symfony 公式サイトからパッケージをダウンロードして展開する、もしくは代わりに Subversion リポジトリへの svn:externals
リンクを作ります。
プラグインはディレクトリに格納されており、symfony 公式サイトからパッケージをダウンロードして展開することも可能で、もしくは代わりに Subversion リポジトリへの svn:externals
リンクからダウンロードできます。
plugin:install
タスクは ProjectConfiguration.class.php
ファイルを自動的に更新することでインストールするプラグインを自動的に有効にします。しかし Subversion を通して、またはアーカイブをダウンロードしてプラグインをインストールする場合、ProjectConfiguration.class.php
で手動で有効にする必要があります。
[php]
// config/ProjectConfiguration.class.php
class ProjectConfiguration extends sfProjectConfiguration
{
public function setup()
{
バックエンドのセキュリティ
新しいプラグインの設定方法を見てみましょう。ユーザー、グループとパーミッションを管理する新しいモデルクラスを提供するので、モデルをリビルドする必要があります。
TIP
propel:build --all --and-load
タスクは既存のすべてのテーブルを再生成する前にこれらを削除することを覚えておいてください。これを避けるには、モデル、フォーム、フィルタをビルドしてから、data/sql/
に保存されている、生成された SQL 文を実行して新しいテーブルを作成します。TIP doctrine:build --all --and-load
タスクは既存のすべてのテーブルを再生成する前にこれらを削除することを覚えておいてください。これを避けるには、モデル、フォーム、フィルタをビルドしてから、data/sql/
に保存されている、生成された SQL 文を実行して新しいテーブルを作成します。
sfGuardPlugin
はユーザークラスにいくつかのメソッドを追加するので、myUser
の基底クラスを sfGuardSecurityUser
に変更する必要があります。
sfDoctrineGuardPlugin
はユーザークラスにいくつかのメソッドを追加するので、myUser
の基底クラスをsfGuardSecurityUser
に変更する必要があります。
[php]
// apps/backend/lib/myUser.class.php
class myUser extends sfGuardSecurityUser
{
}
sfGuardPlugin
はユーザーを認証する signin
アクションを sfGuardAuth
モジュールに提供します。
sfDoctrineGuardPlugin
はユーザーを認証する signin
アクションを sfGuardAuth
モジュールに提供します。
ログインページに使われるデフォルトのアクションを変更するために ~settings.yml
~ ファイルを編集します。
[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
TIP Subversion のトランクから
sfDoctrineGuardPlugin
をインストールしたのであれば、ユーザーを作って昇格させることを一度に行うために次のコマンドを実行する必要があります。$ php symfony guard:create-user fabien@example.com fabien SecretPass Fabien Potencier
TIP
sfGuardPlugin
はコマンドラインからユーザー、グループとパーミッションを管理するタスクを提供します。guard
名前空間に所属するすべてのタスクの一覧を表示するにはlist
タスクを使います。$ php symfony list guard
ユーザーが認証されていないとき、メニューバーを隠す必要があります。
[php]
// 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 ?>
ユーザーが認証されたとき、メニューにログアウトリンクを追加する必要があります。
[php]
// apps/backend/templates/layout.php
<li><?php echo link_to('Logout', 'sf_guard_signout') ?></li>
TIP
sfGuardPlugin
によって提供されるすべてのルートの一覧を表示するには、app:routes
タスクを使います。
TIP
sfDoctrineGuardPlugin
から提供されるすべてのルートの一覧を表示するには、app:routes
タスクを使います。
Jobeet バックエンドにさらに磨きをかけるために、管理者ユーザーを管理する新しいモジュールを追加しましょう。ありがたいことに、sfGuardPlugin
はそのようなモジュールを提供してくれます。sfGuardAuth
モジュールに関して、settings.yml
ファイルでこれを有効にする必要があります。
[yml]
// apps/backend/config/settings.yml
all:
.settings:
enabled_modules: [default, sfGuardAuth, sfGuardUser]
リンクをメニューに追加します。
[php]
// apps/backend/templates/layout.php
<li><?php echo link_to('Users', 'sf_guard_user') ?></li>
完了です!
ユーザーのテスト
ユーザーのテストの話をしていないので13日目は終わっていません。symfony ブラウザは Cookie をシミュレートするので、組み込みの sfTesterUser
テスターによって使うユーザーのふるまいのテストはとても簡単です。
今までに追加したメニュー機能用の機能テストを更新しましょう。job
モジュールの機能テストの末尾に次のコードを追加します。
[php]
// 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 バックエンドをセキュアにすることができました。またプラグインによって提供されたモジュールのおかげで、自由に管理者ユーザーを管理できるクリーンなインターフェイスも追加しました。
ORM