13日目: ユーザー

昨日の内容にはたくさんの情報が詰め込まれていました。ごくわずかな PHP コードと symfony アドミンジェネレータによって、短時間でバックエンドインターフェイスを作ることができます。

今日は、HTTP リクエストの間の永続データを管理する方法を理解します。ご存じの通り、HTTP プロトコルはステートレスです。これはそれぞれのリクエストはその前後のリクエストから独立していることを意味します。現代の Web サイトはユーザーエクスペリエンスを強化するためにリクエストの間のデータを永続化する方法が必要です。

ユーザーセッションは Cookie を利用して識別できます。symfony において、開発者はセッションを直接操作する必要はありませんが、アプリケーションを使うエンドユーザーを表す sfUser オブジェクトを使うことが必要です。

ユーザーフラッシュ

フラッシュを表示するアクションでユーザーオブジェクトをすでに見てきました。フラッシュ (flash) はユーザーセッションに保存され、すぐ次のリクエストの後で自動的に削除される一時的なメッセージです。リダイレクトした後でユーザーにメッセージを表示する必要があるときにとても役立ちます。アドミンジェネレータは求人が保存、削除もしくは延長されるときに、ユーザーにフィードバックを表示するためにフラッシュを良く用います。

フラッシュ

フラッシュは sfUsersetFlash() メソッドを使って設定できます:

[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->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番目は表示するメッセージです。望めばどんなフラッシュ定義できますが、noticeerror はもっとも良く使われるものの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_requestsf_usersf_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
{

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 を完全にするために、履歴をリセットするメソッドを追加しましょう。

[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 ファイルにおいて、ブール値は truefalse の文字列で表わされます。

Web デバッグツールバーのログを見ると、ページにアクセスしようとするたびに defaultActions クラスの executeLogin() メソッドが呼び出されることがわかります。

Web デバッグツールバー

認証されていないユーザーがセキュアなアクションにアクセスしようとすると、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 のバージョンによってグループにまとめられるすべてのバージョンの一覧を示します。

プラグインはディレクトリで自己展開しますが、symfony 公式サイトからパッケージをダウンロードして展開する、もしくは代わりに Subversion リポジトリへの svn:externals リンクを作ります。

プラグインページ は symfony のバージョンでグループ化された利用可能なすべてのバージョンの一覧を表示します。

プラグインはディレクトリに格納されており、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()
  {

$this->enablePlugins(array('sfPropelPlugin', 'sfGuardPlugin')); $this->enablePlugins(array( 'sfDoctrinePlugin', 'sfDoctrineGuardPlugin' )); } }

バックエンドのセキュリティ

それぞれのプラグインには設定方法を説明しているREADME ファイルがあります。 それぞれのプラグインには設定方法を説明しているREADMEファイルがあります。

新しいプラグインの設定方法を見てみましょう。ユーザー、グループとパーミッションを管理する新しいモデルクラスを提供するので、モデルをリビルドする必要があります。

$ php symfony propel:build --all --and-load --no-confirmation $ php symfony doctrine:build --all --and-load --no-confirmation

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 設定に追加することで使いたいモジュールを明示的に有効にする必要があります。

sfGuardPlugin のログイン

最後のステップは 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