Caution: You are browsing the legacy symfony 1.x part of this website.

13日目: ユーザー

1.2 / Doctrine

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

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

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

ユーザーflash

flashつきのアクションでユーザーオブジェクトをすでに見てきました。 flashはユーザーセッションに保存される短期のメッセージです。 これはすぐ次のリクエストの後で自動的に削除されます。 リダイレクトした後でユーザーにメッセージを表示する必要があるときにとても役立ちます。 jobが保存される、削除もしくは延長されるときに、ユーザーにフィードバックを表示するためにadminジェネレーターはflashをたくさん使います。

flash

flashはsfUsersetFlash()メソッドを使って設定できます:

// 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はなんでも定義できますが、noticeerrorはもっとも共通するものの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_requestsf_usersf_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はtruefalse、もしくはonoffの文字列で表現されます。

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

Webデバッグツールバー

認証されていないユーザーがセキュアなアクションにアクセスしようとすると、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のクレデンシャルシステムはとてもシンプルで強力です。 クレデンシャルは(グループもしくはパーミッションのように)アプリケーションのセキュリティモデルを記述するために必要なものを表現できます。

sidebar

複雑なクレデンシャル

複雑なクレデンシャルの要件を記述するためにsecurity.ymlcredentialsエントリーはブール値オペレーションをサポートします。

ユーザーがクレデンシャルABを持たなければならない場合、クレデンシャルを角かっこで囲みます:

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つ: 管理者しかないのでクレデンシャルは使いません。

プラグイン

車輪の再発明をしたくないので、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(設定)に追加することで使いたいモジュールを明示的に有効にする必要があります。

sfGuardPluginのログイン

最後のステップは管理者ユーザーを作成することです:

$ 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バックエンドをセキュアにすることができました。 またプラグインによって提供されたモジュールのおかげで、自由に管理者ユーザーを管理できるクリーンなインターフェイスも追加しました。