Caution: You are browsing the legacy 1.x part of this website.
This version of symfony is not maintained anymore. If some of your projects still use this version, consider upgrading.

10日目: フォーム

Jobeet

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

フォームフレームワーク

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

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

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

フォーム

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

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() メソッドを使って別のフォームに埋め込むことができます:

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

Doctrine フォーム

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

$ php symfony doctrine:build --forms

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

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

tip

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

tip

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

SomeModel:
  options:
    symfony:
      form: false
      filter: false

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

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

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

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

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

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated']
    );
  }
}

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

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

// lib/form/doctrine/JobeetJobForm.class.php
        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 に変更してみましょう:

// lib/form/doctrine/JobeetJobForm.class.php
public function configure()
{
  // ...
 
  $this->validatorSchema['email'] = new sfValidatorEmail();
}

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

// 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 である場合でも、値を選択肢のリスト: フルタイム、パートタイムもしくはフリーランスに制限したい場合を考えてみましょう。

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

// 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 を使います:

$this->widgetSchema['type'] = new sfWidgetFormChoice(array(
  '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 のようなツールを利用することで、ハッカーはウィジェットの選択肢を簡単に回避できます。そこで、値を利用可能な選択肢のみに制限するためにバリデータを変更しましょう:

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

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

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

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

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

$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',
));

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

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

note

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

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

// 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 カラムに追加してみましょう:

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

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

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated']
    );
 
    $this->validatorSchema['email'] = new sfValidatorEmail();
 
    $this->widgetSchema['type'] = new sfWidgetFormChoice(array(
      'choices'  => Doctrine_Core::getTable('JobeetJob')->getTypes(),
      'expanded' => true,
    ));
    $this->validatorSchema['type'] = new sfValidatorChoice(array(
      'choices' => array_keys(Doctrine_Core::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 テンプレートはとても似通っています:

<!-- 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 パーシャルの内容を次のコードで置き換えます:

<!-- 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> タグを生成し、オブジェクトの新規作成かどうかによって HTTP メソッドを POST または PUT へ変更します。フォームがファイル入力タグをもつ場合は、ヘルパーにより 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 foreach ($form as $widget): ?>
  <?php echo $widget->renderRow() ?>
<?php endforeach ?>

フォームのアクション

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

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

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

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

フォームのフロー

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

// 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 アクションの唯一の違いは、修正される求人オブジェクトがフォームコンストラクタの最初の引数として渡されることです。このオブジェクトは、テンプレートのウィジェットのデフォルト値として使われます (Doctrine フォームでは、デフォルト値はオブジェクトですが、シンプルなフォームではプレーンな配列です)。

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

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

// 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() メソッドを次のように更新します:

// 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($conn);
}

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

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated'],
      $this['token']
    );
 
    // ...
  }
 
  // ...
}

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

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

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

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

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

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

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

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

プレビューページ

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

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

<!-- 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 パーシャルを作ります:

<!-- 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 クラスに次のような一連のショートカットメソッドを追加しました:

// lib/model/doctrine/JobeetJob.class.php
public function getTypeName()
{
  $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->getDateTimeObject('expires_at')->format('U') - time()) / 86400);
}

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

無効な求人

有効な求人

note

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

求人の有効化と公開

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

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

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

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

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

// 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() メソッドを呼び出します:

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

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

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

// 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() メソッドを作成したとき、addActiveJobsQuery() メソッドを使うのを忘れていました。ですので、このメソッドを編集して新しい要件を追加する必要があります:

class JobeetCategoryTable extends Doctrine_Table
{
  public function getWithJobs()
  {
    // ...
 
    $q->andWhere('j.is_activated = ?', 1);
 
    return $q->execute();
  }

また明日

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

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