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

10日目: フォーム

1.2 / Doctrine

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

フォームフレームワーク

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

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

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

フォーム

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

class ContactForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'email'   => new sfWidgetFormInput(),
      'message' => new sfWidgetFormTextarea(),
    ));
 
    $this->setValidators(array(
      'email'   => new sfValidatorEmail(),
      'message' => new sfValidatorString(array('max_length' => 255)),
    ));
  }
}

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

tip

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

ウィジェットとバリデーターのクラス名はきわめて明確です: emailフィールドはHTMLの<input>タグとしてレンダリングされ(sfWidgetFormInput)、Eメールアドレスとしてバリデートされます(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タスクを起動したとき、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/サブディレクトリに生成されたファイルを眺めると、組み込みのウィジェットとバリデーターのすばらしい使い方の例がたくさん見つかります。

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

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

最初に、これからの変更をブラウザーですぐにチェックできるように、レイアウトの"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すると、フィールドのウィジェットとバリデーターの両方が削除されます。

フォームの設定は、データベーススキーマから自動生成された内容よりも正確でなければならないことがあります。 たとえば、emailカラムはスキーマでは単なるvarcharですが、実際はEメールとしてバリデートする必要があります。 デフォルトの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::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::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::getTable('JobeetJob')->getTypes(),
      'expanded' => true,
    ));
    $this->validatorSchema['type'] = new sfValidatorChoice(array(
      'choices' => array_keys(Doctrine::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 include_stylesheets_for_form($form) ?>
<?php include_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>

include_javascripts_for_form()include_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($this->generateUrl('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::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 floor((strtotime($this->getExpiresAt()) - time()) / 86400);
}

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

無効な求人

有効な求人

note

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

求人の有効化と公開

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

# apps/frontend/config/routing|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($this->generateUrl('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のフォームフレームワークをより理解していただけることを願っています。

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