10日目: フォーム

Jobeet の2週目は symfony のテストフレームワークの導入で好調な滑り出しを始めました。今日はフォームフレームワークに焦点を合わせます。

フォームフレームワーク

ほとんどの Web サイトには~フォーム~があります。シンプルな問い合わせから、たくさんのフィールドがある複雑なものまで、さまざまなフォームがあります。フォームを作る作業は、Web 開発者にとってもっとも複雑で退屈な作業の1つです。HTML フォームを書き、それぞれのフィールド用のバリデーションルールを実装し、値を処理してデータベースに保存し、エラーメッセージを表示し、エラーの場合はフィールドを再設定することなどが必要です。

もちろん、何度も車輪の再発明をする代わりに、symfony はフォームの管理を簡単にするフレームワークを提供します。 フォームフレームワークは3つの部分で構成されます:

  • バリデーション: ~バリデーション~サブフレームワークは入力 (整数、文字列、メールアドレス・・・) をバリデートするクラス群を提供します。
  • ウィジェット: ~ウィジェット~サブフレームワークは HTML フィールド(入力、テキストエリア、選択・・・) を出力するクラス群を提供します。
  • フォーム: ~フォーム~クラス群はウィジェットとバリデータで構成されるフォームを表し、フォームを管理しやすくするメソッドを提供します。それぞれのフォームフィールドに、個別のバリデータとウィジェットが設定されます。

フォーム

symfony のフォームは1つ以上のフィールドから構成されるクラスです。それぞれのフィールドは名前、~バリデータ~と~ウィジェット~をもちます。次のクラスでは、シンプルな ContactForm を定義しています:

[php]
class ContactForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'email'   => new sfWidgetFormInputText(),
      'message' => new sfWidgetFormTextarea(),
    ));

    $this->setValidators(array(
      'email'   => new sfValidatorEmail(),
      'message' => new sfValidatorString(array('max_length' => 255)),
    ));
  }
}

configure() メソッドのなかで、setValidators()setWidgets() メソッドを使ってフォームフィールドを定義します。

TIP フォームフレームワークには、多くのウィジェットバリデータが搭載されています。API では、すべてのオプション、エラーとデフォルトエラーメッセージが広範囲にわたって説明されています。

ウィジェットとバリデータのクラス名はきわめて明確です: email フィールドは HTML の <input> タグとしてレンダリングされ (sfWidgetFormInputText)、メールアドレスとしてバリデートされます (sfValidatorEmail)。message フィールドは <textarea> タグとしてレンダリングされ (sfWidgetFormTextarea)、255文字以下の文字列でなければなりません (sfValidatorString)。required オプションのデフォルト値は true なので、デフォルトではすべてのフィールドは~必須|必須のフォームフィールド~です。ですので、email のバリデーションの定義は new sfValidatorEmail(array('required' => true)) と同等です。

TIP mergeForm() メソッドを使ってフォームを別のフォームにマージしたり、embedForm() メソッドを使って別のフォームに埋め込むことができます:

[php]
$this->mergeForm(new AnotherForm());
$this->embedForm('name', new AnotherForm());

##ORM## フォーム

たいていの場合、フォームの内容をデータベースに保存する必要があります。symfony はデータベースモデルに関するすべての内容を知っているので、この情報に基づいてフォームを自動的に生成できます。実際、3日目で propel:build --all --and-load タスクを起動したとき、symfony は自動的に propel:build --forms タスクを呼び出しました:

$ php symfony propel:build --forms

propel:build --forms タスクにより、lib/form/ ディレクトリにフォームクラスが生成されます。生成されるファイルの構成は、lib/model/ のものと似ています。それぞれのモデルクラスごとに、対応するフォームクラスが生成されます (たとえば JobeetJob に対応する JobeetJobForm)。このクラスは基底クラスを継承し、デフォルトでは次のように空です:

[php]

// lib/form/JobeetJobForm.class.php // lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { } }

TIP lib/form/base/ サブディレクトリに生成されるファイルを眺めると、組み込みのウィジェットとバリデータのすばらしい使い方の例がたくさん見つかります。 TIP lib/form/doctrine/base/ サブディレクトリに生成されたファイルを眺めると、組み込みのウィジェットとバリデータのすばらしい使い方の例がたくさん見つかります。

-

TIP Doctrine の symfony ビヘイビアにパラメータを渡すことで特定のモデルでのフォーム生成を無効にできます: TIP Propel の symfony ビヘイビアにパラメータを渡すことで特定のモデルでのフォーム生成を無効にできます

[yml] classes: SomeModel: propel_behaviors: symfony: form: false filter: false [yml] SomeModel: options: symfony: form: false filter: false

求人フォームをカスタマイズする

求人フォームは~フォームのカスタマイズ|フォーム (カスタマイズ)~を学ぶための完璧な例です。カスタマイズする方法を順に見てみましょう。

最初に、これからの変更をブラウザですぐにチェックできるように、レイアウトの「Post a Job」リンクを変更します:

[php]
<!-- apps/frontend/templates/layout.php -->
<a href="<?php echo url_for('job_new') ?>">Post a Job</a>

ORM## フォームでは、デフォルトではすべてのテーブルカラムに対応するフィールドが表示されます。求人フォームには、エンドユーザーが編集してはならないフィールドがあります。フォームからフィールドを削除するには、単純にフィールドを unset するだけです:

[php]

// lib/form/JobeetJobForm.class.php // lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['createdat'], $this['updatedat'], $this['expiresat'], $this['isactivated'] ); } }

フィールドを unset すると、フィールドのウィジェットとバリデータの両方が削除されます。

表示したくないフィールドを解除する代わりに、useFields() メソッドを使うことで望むフィールドのリストを明確に用意することもできます:

[php]
<propel>
    // lib/form/JobeetJobForm.class.php
</propel>
<doctrine>
    // lib/form/doctrine/JobeetJobForm.class.php
</doctrine>
    class JobeetJobForm extends BaseJobeetJobForm
    {
      public function configure()
      {
        $this->useFields(array('category_id', 'type', 'company', 'logo',
          ➥ 'url', 'position', 'location', 'description', 'how_to_apply',
          ➥ 'token', 'is_public', 'email'));
      }
    }

useFields() メソッドはあなたのために2つのことを自動的に行います: 隠しフィールドが追加され、フィールドの配列はフィールドの順序を変更するために使われます。

TIP 表示したいフォームフィールドのリストを明示的に示すことは、新しいフィールドを基底フォームに追加するとき、それらのフィールドが自動的にフォームに現れないことを意味します (モデルフォームで新しいカラムを関連テーブルに追加することを想像してください)。

フォームの設定は、データベーススキーマから自動生成された内容よりも正確でなければならないことがあります。たとえば、email カラムはスキーマでは単なる varchar ですが、実際はメールアドレスとしてバリデートする必要があります。デフォルトの sfValidatorStringsfValidatorEmail に変更してみましょう:

[php]

// lib/form/JobeetJobForm.class.php // lib/form/doctrine/JobeetJobForm.class.php public function configure() { // ...

  $this->validatorSchema['email'] = new sfValidatorEmail();
}

このように、デフォルトのバリデータを置き換えることは、常にベストソリューションというわけではありません。データベーススキーマから自動生成されるデフォルトのバリデーションルール (new sfValidatorString(array('max_length' => 255))) が失われるからです。特別な sfValidatorAnd バリデータを使って、新しいバリデータを既存のバリデータに追加するようにしましょう:

[php]

// lib/form/JobeetJobForm.class.php // lib/form/doctrine/JobeetJobForm.class.php public function configure() { // ...

  $this->validatorSchema['email'] = new sfValidatorAnd(array(
    $this->validatorSchema['email'],
    new sfValidatorEmail(),
  ));
}

sfValidatorAnd バリデータは有効な値に対して通るバリデータの配列を受け取ります。ここでは、現在のバリデータを参照し ($this->validatorSchema['email'])、新しいバリデータを追加するトリックを使っています。

NOTE 値が少なくとも1つのバリデータだけを通ればよい場合は、sfValidatorOr バリデータを使うこともできます。そしてもちろん、sfValidatorAndsfValidatorOr バリデータを組み合わせて、複雑なブール値にもとづいたバリデータを作ることができます。

スキーマで type カラムが varchar である場合でも、値を選択肢のリスト: フルタイム、パートタイムもしくはフリーランスに制限したい場合を考えてみましょう。

最初に、利用可能な値を JobeetJobPeer で定義しましょう:

[php]
// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public $types = array(
    'full-time' => 'Full time',
    'part-time' => 'Part time',
    'freelance' => 'Freelance',
  );

  // ...
}

最初に、利用可能な値を JobeetJobTable で定義しましょう:

[php]
// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  static public $types = array(
    'full-time' => 'Full time',
    'part-time' => 'Part time',
    'freelance' => 'Freelance',
  );

  public function getTypes()
  {
    return self::$types;
  }

  // ...
}

type フィールドのウィジェットに sfWidgetFormChoice を使います:

[php]
$this->widgetSchema['type'] = new sfWidgetFormChoice(array(

'choices' => JobeetJobPeer::$types, 'choices' => Doctrine_Core::getTable('JobeetJob')->getTypes(), 'expanded' => true, ));

sfWidgetFormChoice は~選択ウィジェット~を表し、設定オプション (expandedmultiple) の値に応じて、次のような異なるウィジェットをレンダリングできます:

  • ドロップダウンリスト (<select>): array('multiple' => false, 'expanded' => false)
  • ドロップダウンボックス (<select multiple="multiple">): array('multiple' => true, 'expanded' => false)
  • ラジオボタンのリスト: array('multiple' => false, 'expanded' => true)
  • チェックボックスのリスト: array('multiple' => true, 'expanded' => true)

NOTE デフォルトでラジオボタンの1つを選択した状態にしたい場合 (たとえば full-time)、データベーススキーマのデフォルト値を変更します。

このようにすれば、有効ではない値を誰も投稿できないと考えるかもしれません。しかし、curl もしくは Firefox の Web Developer Toolbar のようなツールを利用することで、ハッカーはウィジェットの選択肢を簡単に回避できます。そこで、値を利用可能な選択肢のみに制限するためにバリデータを変更しましょう:

[php] $this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => arraykeys(JobeetJobPeer::$types), )); [php] $this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => arraykeys(Doctrine_Core::getTable('JobeetJob')->getTypes()), ));

logo カラムには求人に関連するロゴのファイル名を保存するので、ウィジェットを~ファイル入力~タグに変更する必要があります:

[php]
$this->widgetSchema['logo'] = new sfWidgetFormInputFile(array(
  'label' => 'Company logo',
));

それぞれのフィールドに対して、symfony は~ラベル|フォームラベル~ (<label> タグに使われる) を自動的に生成します。ラベルは label オプションで変更できます。

setLabels() メソッドを使って、ウィジェット配列のラベルを一括で変更することもできます:

[php]
$this->widgetSchema->setLabels(array(
  'category_id'    => 'Category',
  'is_public'      => 'Public?',
  'how_to_apply'   => 'How to apply?',
));

デフォルトのバリデータを変更する必要もあります:

[php]
$this->validatorSchema['logo'] = new sfValidatorFile(array(
  'required'   => false,
  'path'       => sfConfig::get('sf_upload_dir').'/jobs',
  'mime_types' => 'web_images',
));

sfValidatorFile にはとてもたくさんの機能があります:

  • アップロードされたファイルが Web フォーマットの画像であることをバリデートする (mime_types)
  • ファイルを一意性のある名前にリネームする
  • ファイルを path で指定された場所に保存する
  • 生成されたファイル名で logo カラムを更新する

NOTE ロゴ用のディレクトリ (web/uploads/jobs/) を作り、Web サーバーによって書き込み可能であることを確認してください。

バリデータはデータベースに相対パスを保存するので、showSuccess テンプレートで使われているパスを次のように変更します:

[php]
// apps/frontend/modules/job/templates/showSuccess.php
<img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" />

TIP generateLogoFilename() メソッドがモデルクラスに存在する場合、このメソッドがバリデータによって呼び出され、デフォルトで生成される logo のファイル名をオーバーライドします。メソッドには sfValidatedFile オブジェクトが引数として渡されます。

フィールドごとに生成されるラベルをオーバーライドできるのと同じようにして、~ヘルプメッセージ|ヘルプ (フォーム)~も定義できます。重要性をよりわかりやすく説明するために、ヘルプメッセージを is_public カラムに追加してみましょう:

[php]
$this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');

ここまでで JobeetJobForm クラスは次のようになりました:

[php]

// lib/form/JobeetJobForm.class.php // lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['createdat'], $this['updatedat'], $this['expiresat'], $this['isactivated'] );

    $this->validatorSchema['email'] = new sfValidatorEmail();

    $this->widgetSchema['type'] = new sfWidgetFormChoice(array(

'choices' => JobeetJobPeer::$types, 'choices' => DoctrineCore::getTable('JobeetJob')->getTypes(), 'expanded' => true, )); $this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => arraykeys(JobeetJobPeer::$types), 'choices' => arraykeys(DoctrineCore::getTable('JobeetJob')->getTypes()), ));

    $this->widgetSchema['logo'] = new sfWidgetFormInputFile(array(
      'label' => 'Company logo',
    ));

    $this->widgetSchema->setLabels(array(
      'category_id'    => 'Category',
      'is_public'      => 'Public?',
      'how_to_apply'   => 'How to apply?',
    ));

    $this->validatorSchema['logo'] = new sfValidatorFile(array(
      'required'   => false,
      'path'       => sfConfig::get('sf_upload_dir').'/jobs',
      'mime_types' => 'web_images',
    ));

    $this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');
  }
}

フォームのテンプレート

フォームのカスタマイズが完了したので、次はフォームを表示してみましょう。新しい求人を作成もしくは既存の求人を編集する場合のフォームの~テンプレート~は同じです。実際、newSuccess.phpeditSuccess.php テンプレートはとても似通っています:

[php]
<!-- apps/frontend/modules/job/templates/newSuccess.php -->
<?php use_stylesheet('job.css') ?>

<h1>Post a Job</h1>

<?php include_partial('form', array('form' => $form)) ?>

NOTE まだ job スタイルシートを追加していないので、両方のテンプレートに追加しましょう (<?php use_stylesheet('job.css') ?>)。

フォーム自身は _form ~パーシャル|パーシャルテンプレート~でレンダリングされます。生成された _form パーシャルの内容を次のコードで置き換えます:

[php]
<!-- apps/frontend/modules/job/templates/_form.php -->
<?php use_stylesheets_for_form($form) ?>
<?php use_javascripts_for_form($form) ?>

<?php echo form_tag_for($form, '@job') ?>
  <table id="job_form">
    <tfoot>
      <tr>
        <td colspan="2">
          <input type="submit" value="Preview your job" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
</form>

use_javascripts_for_form()use_stylesheets_for_form() ヘルパーは、フォームウィジェットに必要な JavaScript とスタイルシートをインクルードします。

TIP 求人フォームが ~JavaScript~ もしくは~スタイルシート~を必要としなくても、万が一に備えてこれらのヘルパーを呼び出しておくのはよい習慣です。ウィジェットを JavaScript もしくは固有のスタイルシートを必要とするウィジェットに変更する場合、後で時間の節約になります。

~form_tag_for() ヘルパー~は、渡されたフォームとルートに対応する <form> ~タグ|フォーム (HTML) ~を生成し、オブジェクトの新規作成かどうかによって ~HTTP メソッド~を ~POST|POST (HTTP メソッド)~ または ~PUT|PUT (HTTP メソッド)~ へ変更します。フォームがファイル入力タグをもつ場合は、ヘルパーにより ~multipart|フォーム (Multipart)~ 属性が自動的に加えられます。

最後に、<?php echo $form ?> によってフォームウィジェットがレンダリングされます。

SIDEBAR フォームの外見をカスタマイズする

デフォルトでは、<?php echo $form ?> はフォームウィジェットをテーブルの行としてレンダリングします。

たいていの場合、フォームのレイアウトをカスタマイズする必要があります。フォームオブジェクトには、フォームを~カスタマイズ~するための多くの便利なメソッドがあります:

| メソッド | 説明 | ---------------------- | ------------------------------------------------------ | render() | フォームをレンダリングする (echo $form の出力と同等) | renderHiddenFields() | 隠しフィールドをレンダリングする | hasErrors() | フォームにエラーがある場合 true を返す | hasGlobalErrors() | フォームにグローバルエラーがある場合 true を返す | getGlobalErrors() | グローバルエラーの配列を返す | renderGlobalErrors() | グローバルエラーをレンダリングする

フォームはフィールドの配列のようにもふるまいます。$form['company'] によって company フィールドにアクセスできます。返されるオブジェクトはフィールドのそれぞれの要素をレンダリングするメソッドを提供します:

| メソッド | 説明 | --------------- | ----------------------------------------------------------- | renderRow() | フィールドの行をレンダリングする | render() | フィールドのウィジェットをレンダリングする | renderLabel() | フィールドのラベルをレンダリングする | renderError() | フィールドのエラーメッセージがある場合にレンダリングする | renderHelp() | フィールドのヘルプメッセージをレンダリングする

echo $form ステートメントは次のコードと同等です:

[php]
<?php foreach ($form as $widget): ?>
  <?php echo $widget->renderRow() ?>
<?php endforeach ?>

フォームのアクション

フォームクラスと、フォームをレンダリングするテンプレートが用意できました。では、実際にこれらを~アクション~と連携させてみましょう。

求人のフォームは job モジュールの次の5つのメソッドで管理されます:

  • new: 新しい求人を作成する空白のフォームを表示する
  • edit: 既存の求人を編集するフォームを表示する
  • create: ユーザーが投稿した値で新しい求人を作成する
  • update: ユーザーが投稿した値で既存の求人を更新する
  • processForm: createupdate によって呼び出され、フォームを処理する (バリデーション、フォームの再設定、およびデータベースへの保存)

すべてのフォームには次のようなライフサイクルがあります:

フォームのフロー

5日前に job モジュール用の ##ORM## ルートコレクションを作ったので、フォーム管理メソッド用のコードを次のように簡略化できます:

[php]
// apps/frontend/modules/job/actions/actions.class.php
public function executeNew(sfWebRequest $request)
{
  $this->form = new JobeetJobForm();
}

public function executeCreate(sfWebRequest $request)
{
  $this->form = new JobeetJobForm();
  $this->processForm($request, $this->form);
  $this->setTemplate('new');
}

public function executeEdit(sfWebRequest $request)
{
  $this->form = new JobeetJobForm($this->getRoute()->getObject());
}

public function executeUpdate(sfWebRequest $request)
{
  $this->form = new JobeetJobForm($this->getRoute()->getObject());
  $this->processForm($request, $this->form);
  $this->setTemplate('edit');
}

public function executeDelete(sfWebRequest $request)
{
  $request->checkCSRFProtection();

  $job = $this->getRoute()->getObject();
  $job->delete();

  $this->redirect('job/index');
}

protected function processForm(sfWebRequest $request, sfForm $form)
{
  $form->bind(
    $request->getParameter($form->getName()),
    $request->getFiles($form->getName())
  );

  if ($form->isValid())
  {
    $job = $form->save();

    $this->redirect('job_show', $job);
  }
}

/job/new ページにアクセスすると、新しい~フォーム~インスタンスが作成され、テンプレートに渡されます (new アクション)。

ユーザーがフォームを投稿すると (create アクション)、ユーザーが投稿した値がフォームにバインドされ (bind() メソッド)、バリデーションが実行されます。

フォームがバインドされると、isValid() メソッドを利用して有効性をチェックできます: フォームが有効な場合 (true を返す)、求人がデータベースに保存され ($form->save())、ユーザーは求人のプレビューページに~リダイレクト~されます; フォームが有効でない場合、ユーザーが投稿した値と関連するエラーメッセージが設定された newSuccess.php テンプレートが再表示されます。

TIP setTemplate() メソッドを呼び出すと、アクションで使う~テンプレート~を変更できます。投稿されたフォームが有効でない場合、createupdate メソッドはエラーメッセージを伴うフォームを再表示するためにそれぞれ newedit アクションと同じテンプレートを使います。

既存の求人の修正は、作成ととても似ています。newedit アクションの唯一の違いは、修正される求人オブジェクトがフォームコンストラクタの最初の引数として渡されることです。このオブジェクトは、テンプレートのウィジェットのデフォルト値として使われます (##ORM## フォームでは、デフォルト値はオブジェクトですが、シンプルなフォームではプレーンな配列です)。

求人の作成フォームにデフォルト値を定義することもできます。フォームのデフォルト値を定義する1つのやり方は、データベーススキーマのなかでデフォルト値を宣言することです。別のやり方は、フォームコンストラクタにあらかじめ修正した Job オブジェクトを渡すことです。

type カラムのデフォルト値として full-time を定義するために executeNew() メソッドを次のように変更します:

[php]
// apps/frontend/modules/job/actions/actions.class.php
public function executeNew(sfWebRequest $request)
{
  $job = new JobeetJob();
  $job->setType('full-time');

  $this->form = new JobeetJobForm($job);
}

NOTE フォームがバインドされると、デフォルトの値はユーザー投稿の値に置き換えられます。バリデーションエラーでフォームが再表示される場合、ユーザーが投稿した値がフォームの再設定に使われます。

トークンで求人フォームを保護する

そろそろ完成が近づいてきました。ただし今のところ、求人に対するトークンをユーザーが入力しなければなりません。一意性をもつトークンの取得をユーザーに依存したくないので、新しい求人が作られるときに求人トークンが自動的に生成されるようにします。

新しい求人が保存される前にトークンを生成するロジックを追加するように、JobeetJobsave() メソッドを次のように更新します:

[php]

// lib/model/JobeetJob.php public function save(PropelPDO $con = null) // lib/model/doctrine/JobeetJob.class.php public function save(Doctrine_Connection $conn = null) { // ...

  if (!$this->getToken())
  {
    $this->setToken(sha1($this->getEmail().rand(11111, 99999)));
  }

return parent::save($con); return parent::save($conn); }

これでフォームから token フィールドを取り除くことができます:

[php]

// lib/form/JobeetJobForm.class.php // lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['createdat'], $this['updatedat'], $this['expiresat'], $this['isactivated'], $this['token'] );

    // ...
  }

  // ...
}

2日目のユーザーストーリーで示したように、求人情報は、ユーザーが関連トークンを知っている場合にのみ編集できます。しかし現時点では、きわめて簡単に URL を推測して求人を編集もしくは削除できてしまいます。求人の~主キー~を ID とすると、編集フォームの URL は /job/ID/edit のようになるからです。

デフォルトでは、~sfPropelRouteCollection~ ルートは主キーを使う URL を生成しますが、次のように column オプションを指定することで一意性をもつ値をもつ別のカラムに変更できます:

[yml]
# apps/frontend/config/routing.yml
job:
  class:        sfPropelRouteCollection
  options:      { model: JobeetJob, column: token }
  requirements: { token: \w+ }

ユニークキーに対する symfony のデフォルトの要件が \d+ なので、任意の文字列にマッチさせるために token パラメータの要件も変更したことにご注意ください。

job に関連するすべてのルートは、job_show_user のものを除いて、トークンを埋め込みます。たとえば、job を編集するルートは次のパターンで構成されます:

http://jobeet.localhost/job/TOKEN/edit

showSuccess テンプレートの「Edit」リンクも変更してください:

[php]
<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<a href="<?php echo url_for('job_edit', $job) ?>">Edit</a>

プレビューページ

プレビューページは job ページの表示と同じです。~ルーティング~のおかげで、ユーザーが正しいトークンでアクセスした場合は、token リクエストパラメータにアクセスできます。

ユーザーがトークンを使用する URL でアクセスした場合、管理バーがページのトップに追加されます。showSuccess テンプレートの始めに管理バーをホストするパーシャルを追加し、末尾の edit リンクを削除します:

[php]
<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<?php if ($sf_request->getParameter('token') == $job->getToken()): ?>
  <?php include_partial('job/admin', array('job' => $job)) ?>
<?php endif ?>

_admin パーシャルを作ります:

[php]
<!-- apps/frontend/modules/job/templates/_admin.php -->
<div id="job_actions">
  <h3>Admin</h3>
  <ul>
    <?php if (!$job->getIsActivated()): ?>
      <li><?php echo link_to('Edit', 'job_edit', $job) ?></li>
      <li><?php echo link_to('Publish', 'job_edit', $job) ?></li>
    <?php endif ?>
    <li><?php echo link_to('Delete', 'job_delete', $job, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?></li>
    <?php if ($job->getIsActivated()): ?>
      <li<?php $job->expiresSoon() and print ' class="expires_soon"' ?>>
        <?php if ($job->isExpired()): ?>
          Expired
        <?php else: ?>
          Expires in <strong><?php echo $job->getDaysBeforeExpires() ?></strong> days
        <?php endif ?>

        <?php if ($job->expiresSoon()): ?>
         - <a href="">Extend</a> for another <?php echo sfConfig::get('app_active_days') ?> days
        <?php endif ?>
      </li>
    <?php else: ?>
      <li>
        [Bookmark this <?php echo link_to('URL', 'job_show', $job, true) ?> to manage this job in the future.]
      </li>
    <?php endif ?>
  </ul>
</div>

たくさんのコードがありますが、たいていのコードは簡単に理解できます。

テンプレートを読みやすくするために、JobeetJob クラスに次のような一連のショートカットメソッドを追加しました:

[php]

// lib/model/JobeetJob.php // lib/model/doctrine/JobeetJob.class.php public function getTypeName() { return $this->getType() ? JobeetJobPeer::$types[$this->getType()] : ''; $types = Doctrine_Core::getTable('JobeetJob')->getTypes(); return $this->getType() ? $types[$this->getType()] : ''; }

public function isExpired()
{
  return $this->getDaysBeforeExpires() < 0;
}

public function expiresSoon()
{
  return $this->getDaysBeforeExpires() < 5;
}

public function getDaysBeforeExpires()
{

return ceil(($this->getExpiresAt('U') - time()) / 86400); return ceil(($this->getDateTimeObject('expires_at')->format('U') - time()) / 86400); }

管理バーには、求人のステータスに合わせて異なるアクションが表示されます:

無効な求人

有効な求人

NOTE 次のセクションの後で「activated」バーを見ることができます。

求人の有効化と公開

前のセクションで、求人を公開するリンクがありました。このリンクが新しい publish アクションを指し示すように変更する必要があります。新しい~ルート~を作る代わりに、既存の job ルートを次のように設定します:

[yml]
# apps/frontend/config/routing.yml
job:
  class:   sfPropelRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: put }
  requirements:
    token: \w+

object_actions は与えられたオブジェクト用の追加アクションの配列をとります。「Publish」リンクを変更しましょう:

[php]
<!-- apps/frontend/modules/job/templates/_admin.php -->
<li>
  <?php echo link_to('Publish', 'job_publish', $job, array('method' => 'put')) ?>
</li>

最後のステップは publish アクションを作ることです:

[php]
// apps/frontend/modules/job/actions/actions.class.php
public function executePublish(sfWebRequest $request)
{
  $request->checkCSRFProtection();

  $job = $this->getRoute()->getObject();
  $job->publish();

  $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days')));

  $this->redirect('job_show_user', $job);
}

賢明な読者は「Publish」リンクが HTTP PUT メソッドで投稿されることにお気づきでしょう。PUT メソッドをシミュレートするために、リンクがクリックされると自動的にフォームに変換されます。

~CSRF~ の保護を有効にしたので、link_to() ヘルパーによって CSRF トークンがリンクに埋め込まれます。リクエストオブジェクトの checkCSRFProtection() メソッドを使って、投稿された CSRF トークンの有効性をチェックできます。

executePublish() メソッドでは、次のように定義される新しい publish() メソッドを呼び出します:

[php]

// lib/model/JobeetJob.php // lib/model/doctrine/JobeetJob.class.php public function publish() { $this->setIsActivated(true); $this->save(); }

これで、新しい公開機能をブラウザでテストできます。

しかし、修正するものがまだあります。有効化されていない求人情報はアクセスされてはなりません。つまり、Jobeet ホームページにこれらの求人情報は表示されてはならず、URL からアクセス可能であってはならないことです。Criteria を有効な求人情報のみに制限する addActiveJobsCriteria() メソッドをすでに作ったので、最後にこのメソッドを編集して新しい要件を追加します: しかし、修正するものがまだあります。有効化されていない求人情報はアクセスされてはなりません。つまり、Jobeet ホームページにこれらの求人情報は表示されてはならず、URL からアクセス可能であってはならないことです。Doctrine_Query を有効な求人情報のみに制限する addActiveJobsQuery() メソッドをすでに作ったので、最後にこのメソッドを編集して新しい要件を追加します:

[php] // lib/model/JobeetJobPeer.php static public function addActiveJobsCriteria(Criteria $criteria = null) { // ...

  $criteria->add(self::IS_ACTIVATED, true);

  return $criteria;
}

[php] // lib/model/doctrine/JobeetJobTable.class.php public function addActiveJobsQuery(Doctrine_Query $q = null) { // ...

  $q->andWhere($alias . '.is_activated = ?', 1);

  return $q;
}

これでお終いです。ブラウザでテストしてみましょう。すべての有効ではない求人はホームページから消えました; それらの URL を知っていても、もはやアクセスできません。しかしながら、これらは求人のトークンURLを知っていればアクセスできます。この場合、求人のプレビューは管理バーつきで表示されます。

これまでのところ、これが MVC パターンとリファクタリングの大きな利点の1つです。新しい要件を追加するために1つのメソッドで1つの変更だけが必要でした。

NOTE getWithJobs() メソッドを作成したとき、addActiveJobsCriteria() メソッドを使うのを忘れていました。ですので、このメソッドを編集して新しい要件を追加する必要があります: getWithJobs() メソッドを作成したとき、addActiveJobsQuery() メソッドを使うのを忘れていました。ですので、このメソッドを編集して新しい要件を追加する必要があります:

[php] class JobeetCategoryPeer extends BaseJobeetCategoryPeer { static public function getWithJobs() { // ...

    $criteria->add(JobeetJobPeer::IS_ACTIVATED, true);

    return self::doSelect($criteria);
  }

[php] class JobeetCategoryTable extends Doctrine_Table { public function getWithJobs() { // ...

    $q->andWhere('j.is_activated = ?', 1);

    return $q->execute();
  }

また明日

今日のチュートリアルにはたくさんの新しい情報が詰め込まれていましたが、symfony のフォームフレームワークをより理解していただけることを願っています。

今日私たちが忘れたことにお気づきの方がいらっしゃることは承知しております。新しい機能に対してテストを実装しませんでした。テストを書くことはアプリケーションの開発の重要な部分なので、これは明日の最初に行います。

ORM