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

第4章 - Propelとの統合

Symfony version
Language

Webプロジェクトにおいて、たいていのフォームはモデルオブジェクトを作るもしくは修正するために使われます。ORMのおかげでこれらのオブジェクトは通常はデータベースでシリアライズされます。symfonyのフォームシステムはsymfonyに組み込まれているPropelと連動する追加レイヤーを提供し、これらのモデルオブジェクトに基づいたフォームの実装をより簡単にします。

この章ではフォームをPropelのオブジェクトモデルと統合する方法を詳しく検討します。Propelとsymfonyとの統合機能に習熟していることが大いに推奨されます。そうではない場合、"The Definitive Guide to symfony"のモデルレイヤーの内側の章を参照してください。

始める前に

この章では、記事の管理システムを作ります。データベースのスキーマから始めましょう。リスト4-1で示されるように、これは次の5つのテーブル: articleauthorcategorytag、とarticle_tagから構成されます。

リスト4-1 - データベーススキーマ

// config/schema.yml
propel:
  article:
    id:           ~
    title:        { type: varchar(255), required: true }
    slug:         { type: varchar(255), required: true }
    content:      longvarchar
    status:       varchar(255)
    author_id:    { type: integer, required: true, foreignTable: author, foreignReference: id, OnDelete: cascade }
    category_id:  { type: integer, required: false, foreignTable: category, foreignReference: id, onDelete: setnull }
    published_at: timestamp
    created_at:   ~
    updated_at:   ~
    _uniques:
      unique_slug: [slug]
 
  author:
    id:           ~
    first_name:   varchar(20)
    last_name:    varchar(20)
    email:        { type: varchar(255), required: true }
    active:       boolean
 
  category:
    id:           ~
    name:         { type: varchar(255), required: true }
 
  tag:
    id:           ~
    name:         { type: varchar(255), required: true }
 
  article_tag:
    article_id:   { type: integer, foreignTable: article, foreignReference: id, primaryKey: true, onDelete: cascade }
    tag_id:       { type: integer, foreignTable: tag, foreignReference: id, primaryKey: true, onDelete: cascade }

テーブル間のリレーションは下記の通りです:

  • articleテーブルとauthorテーブル間の一対多のリレーション: 記事は一人の執筆者だけが書く
  • articleテーブルとcategoryテーブル間の一対多のリレーション: 記事は1つのカテゴリに所属するもしくは所属しない
  • articletagテーブル間の多対多のリレーション

フォームクラスを生成する

articleauthorcategory、とtagテーブルの情報を編集したい場合を考えます。そのためには、これらのテーブルのそれぞれにリンクするフォームを作りデータベースのスキーマに関連するウィジェットとバリデータを設定する必要があります。これらのフォームを手動で作ることが可能であっても、長く退屈なタスクで、全体的に、複数のファイルで同じ種類の情報(カラムとフィールド名、カラムとフィールドの最大サイズ、...)の反復作業が強制されます。さらに、モデルを変更するたびに、関連するフォームクラスも変更しなければなりません。幸いにして、Propelのプラグインはオブジェクトモデルに関連するフォームを生成する処理を自動化する組み込みのpropel:build-formsタスクを用意しています:

$ ./symfony propel:build-forms

フォーム生成の間、タスクはテーブル間のリレーションを考慮しモデルのイントロスペクト機能を使用してテーブルごとの1つのクラスとそれぞれのカラム用のウィジェットを作ります。

note

propel:build-allpropel:build-all-loadpropel:build-formsタスクを自動的に起動させてフォームクラスも更新します。

これらのタスクを実行した後で、ファイル構造はlib/form/ディレクトリに作られます。我々の例題のスキーマ用に作られたファイルは下記の通りです:

lib/
  form/
    BaseFormPropel.class.php
    ArticleForm.class.php
    ArticleTagForm.class.php
    AuthorForm.class.php
    CategoryForm.class.php
    TagForm.class.php
    base/
      BaseArticleForm.class.php
      BaseArticleTagForm.class.php
      BaseAuthorForm.class.php
      BaseCategoryForm.class.php
      BaseTagForm.class.php

propel:build-formsタスクはスキーマのそれぞれのテーブルに対して2つのクラスを生成し、1つの基底クラスはlib/form/baseディレクトリに、もう一つのクラスはlib/form/ディレクトリに存在します。例えばauthorテーブルは、lib/form/base/BaseAuthorForm.class.phplib/form/AuthorForm.class.phpファイルに生成されたBaseAuthorFormAuthorFormクラスで構成されます。

sidebar

フォーム生成のディレクトリ

propel:build-formsタスクはPropelの構造に似た構造でこれらのファイルを生成します。Propelスキーマのpackage属性によってテーブルのサブセットを論理的にまとめることができます。デフォルトのパッケージはlib.modelなので、Propelはlib/model/ディレクトリでこれらのファイルを生成しlib/formディレクトリでフォームが生成されます。下記の例のようにlib.model.cmsパッケージを使うことで、Propelのクラスはlib/model/cms/ディレクトリに生成されフォームクラスはlib/form/cms/ディレクトリに生成されます。

propel:
  _attributes: { noXsd: false, defaultIdMethod: none, package: lib.model.cms }
  # ...

5章で見るようにパッケージはデータベースのスキーマを分割してプラグインの範囲内でフォームを配信するために役立ちます。

Propelのパッケージに関する詳細な情報は、"The Definitive Guide to symfony"のモデルレイヤーの内側の章を参照してください。

下記のテーブルはAuthorFormフォームの定義に必要な異なるクラス間の階層をまとめています。

クラス パッケージ 対象者 説明
AuthorForm project developer 生成したフォームを上書きする
BaseAuthorForm project symfony スキーマに基づきpropel:build-formsタスクの実行ごとに上書きされる
BaseFormPropel project developer Propelのフォームのグローバルなカスタマイゼーションを可能にする
sfFormPropel Propel plugin symfony Propelフォームのベース
sfForm symfony symfony symfonyフォームのベース

Authorクラスからオブジェクトを作るもしくは編集するには、リスト4-2で説明されるように、AuthorFormクラスを使います。お気づきのように、このクラスは設定を通して生成されたBaseAuthorFormを継承するのでメソッドを持ちません。AuthorFormクラスはフォームの設定をカスタマイズして上書きするために使うクラスです。

リスト4-2 - AuthorFormクラス

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
  }
}

リスト4-3はauthorテーブル用のモデルをイントロスペクトするバリデータと生成されたウィジェットを持つBaseAuthorFormクラスを表示します。

リスト4-3 - authorテーブルのためのフォームを表すBaseAuthorFormクラス

class BaseAuthorForm extends BaseFormPropel
{
  public function setup()
  {
    $this->setWidgets(array(
      'id'         => new sfWidgetFormInputHidden(),
      'first_name' => new sfWidgetFormInput(),
      'last_name'  => new sfWidgetFormInput(),
      'email'      => new sfWidgetFormInput(),
    ));
 
    $this->setValidators(array(
      'id'         => new sfValidatorPropelChoice(array('model' => 'Author', 'column' => 'id', 'required' => false)),
      'first_name' => new sfValidatorString(array('max_length' => 20, 'required' => false)),
      'last_name'  => new sfValidatorString(array('max_length' => 20, 'required' => false)),
      'email'      => new sfValidatorString(array('max_length' => 255)),
    ));
 
    $this->widgetSchema->setNameFormat('author[%s]');
 
    $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);
 
    parent::setup();
  }
 
  public function getModelName()
  {
    return 'Author';
  }
}

生成クラスは以前の章ですでに作成したフォームと非常に似ていますが、下記の内容が異なります:

  • 基底クラスはsfFormの代わりにBaseFormPropelです
  • バリデータとウィジェットの設定はconfigure()メソッドの代わりにsetup()メソッドです
  • getModelName()メソッドはこのフォームに関連するPropelクラスを返します

sidebar

Propelフォームのグローバルなカスタマイズ

それぞれのテーブル用の生成クラスに加えて、propel:build-formsBaseFormPropelクラスも生成します。この空のクラスはlib/form/base/ディレクトリに生成された他のすべてのクラスの基底クラスでこれによってすべてのPropelフォームのビヘイビアをグローバルに設定できます。例えば、すべてのPropelフォームに対してデフォルトのフォーマッターを変更することは簡単にできます:

abstract class BaseFormPropel extends sfFormPropel
{
  public function setup()
  {
    sfWidgetFormSchema::setDefaultFormFormatterName('div');
  }
}

BaseFormPropelクラスはsfFormPropelクラスを継承することがわかります。このクラスはPropelとフォームに投稿された値からデータベース内でオブジェクトシリアライズを扱う別の機能の間で固有な機能と連携します。

tip

基底クラスは設定に対してconfigure()メソッドの代わりにsetup()メソッドを使います。この機能によって開発者はparent::configure()呼び出しを対処せずに空の生成クラスの設定を上書きできます。

フォームのフィールドはスキーマで設定したカラム名: idfirst_namelast_name、`emailと全く同じです。

authorテーブルのそれぞれのカラムのために、propel:build-formsタスクはスキーマの定義に従ってウィジェットとバリデータを生成します。タスクは常に可能な限り最もセキュアなバリデータを生成します。idフィールドを考えてみましょう。値が有効な整数であるかどうかチェックできます。代わりにここで生成されたバリデータによって(既存のオブジェクトを編集するために)識別子が実際に存在するかどうかもしくは(新しいオブジェクトが作れるように)識別子が空であることをバリデートできます。これはより強いバリデーションです。

生成されたフォームは即座に利用できます。<?php echo $form ?>ステートメントを追加すれば、これによって一行のコードを書かずにバリデーションで機能を持つフォームを作成できます。

素早くプロトタイプを作る機能を越えて、生成クラスを修正せずに生成フォームを拡張するのが楽です。これは基底とフォームクラスの継承メカニズムのおかげです。

最終的に、データベーススキーマを改良するたびにカスタマイズを上書きすることなくスキーマの変更を考慮するフォームを再生成することが可能となります。

CRUDジェネレータ

これで生成フォームクラスが用意されたので、ブラウザからオブジェクトを処理するsymfonyモジュールを作るのがどれだけ簡単か見てみましょう。ArticleAuthorCategory、とTagクラスからオブジェクトを作る、修正する、削除することを考えます。 Authorクラス用のモジュールを作ることから始めましょう。手作業でモジュールを作ることができても、PropelのプラグインはPropelのオブジェクトモデルクラスに基づいてCRUDモジュールを生成するpropel:generate-crudタスクを提供します。以前のセクションで生成したフォームを使います:

$ ./symfony propel:generate-crud frontend author Author

propel:generate-crudは3つの引数を受け取ります:

  • frontend : 作りたいモジュールを含むアプリケーションの名前
  • author : 作りたいモジュールの名前
  • Author : 作りたいモジュールのためのモデルクラスの名前

note

CRUDはCreation / Retrieval / Update / Deletionを表しモデルデータを実行する基本的な4つのオペレーションを要約しています。

リスト4-4において、Authorクラスのオブジェクトを表示する(index)、作る(create)、修正する(edit)、保存する(update)、そして削除する(delete)ことができる5つのアクションをタスクが生成されたことを見ます。

リスト4-4 - タスクによって生成されたauthorActionsクラス

// apps/frontend/modules/author/actions/actions.class.php
class authorActions extends sfActions
{
  public function executeIndex()
  {
    $this->authorList = AuthorPeer::doSelect(new Criteria());
  }
 
  public function executeCreate()
  {
    $this->form = new AuthorForm();
 
    $this->setTemplate('edit');
  }
 
  public function executeEdit($request)
  {
    $this->form = new AuthorForm(AuthorPeer::retrieveByPk($request->getParameter('id')));
  }
 
  public function executeUpdate($request)
  {
    $this->forward404Unless($request->isMethod('post'));
 
    $this->form = new AuthorForm(AuthorPeer::retrieveByPk($request->getParameter('id')));
 
    $this->form->bind($request->getParameter('author'));
    if ($this->form->isValid())
    {
      $author = $this->form->save();
 
      $this->redirect('author/edit?id='.$author->getId());
    }
 
    $this->setTemplate('edit');
  }
 
  public function executeDelete($request)
  {
    $this->forward404Unless($author = AuthorPeer::retrieveByPk($request->getParameter('id')));
 
    $author->delete();
 
    $this->redirect('author/index');
  }
}

このモジュールにおいて、フォームのライフサイクルは3つのメソッド: createeditと、updateによって対処されます。--non-atomic-actionsオプションによって、propel:generate-crudタスクに以前の3つのメソッドの機能を変換する1つのメソッドのみを生成することを求めることも可能です:

$ ./symfony propel:generate-crud frontend author Author --non-atomic-actions

--non-atomic-actions(リスト4-5)を使用して生成されたコードはより簡潔で冗長ではありません。

リスト4-5 - --non-atomic-actionsオプションで生成されたauthorActionsクラス

class authorActions extends sfActions
{
  public function executeIndex()
  {
    $this->authorList = AuthorPeer::doSelect(new Criteria());
  }
 
  public function executeEdit($request)
  {
    $this->form = new AuthorForm(AuthorPeer::retrieveByPk($request->getParameter('id')));
 
    if ($request->isMethod('post'))
    {
      $this->form->bind($request->getParameter('author'));
      if ($this->form->isValid())
      {
        $author = $this->form->save();
 
        $this->redirect('author/edit?id='.$author->getId());
      }
    }
  }
 
  public function executeDelete($request)
  {
    $this->forward404Unless($author = AuthorPeer::retrieveByPk($request->getParameter('id')));
 
    $author->delete();
 
    $this->redirect('author/index');
  }
}

タスクは2つのテンプレート、indexSuccesseditSuccessも生成しました。editSuccessテンプレートは<?php echo $form ?>ステートメントを使わずに生成されました。--non-verbose-templatesを利用して、このふるまいを修正できます:

$ ./symfony propel:generate-crud frontend author Author --non-verbose-templates

リスト4-6が示すように、このオプションはプロトタイプのフェーズで役に立ちます。

リスト4-6 - editSuccessテンプレート

// apps/frontend/modules/author/templates/editSuccess.php
<?php $author = $form->getObject() ?>
<h1><?php echo $author->isNew() ? 'New' : 'Edit' ?> Author</h1>
 
<form action="<?php echo url_for('author/edit'.(!$author->isNew() ? '?id='.$author->getId() : '')) ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>
  <table>
    <tfoot>
      <tr>
        <td colspan="2">
          &nbsp;<a href="<?php echo url_for('author/index') ?>">Cancel</a>
          <?php if (!$author->isNew()): ?>
            &nbsp;<?php echo link_to('Delete', 'author/delete?id='.$author->getId(), array('post' => true, 'confirm' => 'Are you sure?')) ?>
          <?php endif; ?>
          <input type="submit" value="Save" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
</form>

tip

--with-showオプションによってオブジェクトを閲覧する(読み込みのみ)ために利用できるアクションとテンプレートが生成されます。

生成モジュールを閲覧するためにブラウザで/frontend_dev.php/authorのURLを開くことができます(図4-1と図4-2)。インターフェイスを少し遊んでみてください。生成モジュールのおかげで著者の一覧を表示する、新しい著者を追加する、編集、修正、削除することさえできます。バリデーションルールが機能していることもおわかり頂けるでしょう。

図4-1 - 著者のリスト

著者のリスト

図4-2 - バリデーションエラーがある著者を編集する

バリデーションエラーがある著者を編集する

Articleクラスでオペレーションを繰り返すことができます:

$ ./symfony propel:generate-crud frontend article Article --non-verbose-templates --non-atomic-actions

生成コードはAuthorクラスのコードとよく似ています。しかしながら、新しい記事を作ろうとすると、図4-3で見られるように致命的なエラーが投げられます。

図4-3 - リンクされたテーブルは__toString()メソッドを定義しなければならない

リンクされたテーブルは<code>__toString()</code>メソッドを定義しなければならない

ArticleFormフォームはArticleオブジェクトとAuthorオブジェクト間のリレーションを表現するためにsfWidgetFormPropelSelectウィジェットを使用します。このウィジェットは著者のドロップダウンリストを作ります。表示の間、Authorオブジェクトは__toString()マジックメソッドを使用して文字の文字列に変換されます。リスト4-7で示されるように、Authorクラスで定義されなければなりません。

リスト4-7 - Authorクラス用の__toString()メソッドを実装する

class Author extends BaseAuthor
{
  public function __toString()
  {
    return $this->getFirstName().' '.$this->getLastName();
  }
}

Authorクラスのように、モデルの他のクラス: ArticleCategoryTag用に__toString()メソッドを作ることができます。

tip

sfWidgetFormPropelSelectウィジェットのmethodオプションはテキストフォーマットでオブジェクトを表現するために使われるメソッドを変更します。

図4-4は__toString()メソッドで実装した後に記事を作る方法を示しています。

図4-4 - 記事を作る

記事を作る

生成されたフォームをカスタマイズする

propel:build-formspropel:generate-crudタスクによってモデルオブジェクトを表示する、作成する、編集し、削除する機能を持つsymfonyモジュールを作ることができます。これらのモジュールはモデルのバリデーションルールだけでなくテーブル間のリレーションも考慮します。一行も書かずにこれらすべてが行われます!

生成コードをカスタマイズする段階に来ました。フォームクラスがすでに多くの要素を考慮しているのであれば、いくつかの面をカスタマイズする必要があります。

バリデータとウィジェットを設定する

デフォルトで生成されたバリデータとウィジェットを設定することから始めましょう。

ArticleFormフォームはslugフィールドを持ちます。スラッグは記事をURLで一意的に表現する文字の文字列です。例えば、タイトルが"Optimize the developments with symfony"である記事のスラッグは12-optimize-the-developments-with-symfonyで、12は記事のidです。このフィールドはtitleに依存しており、オブジェクトが保存されたときに自動的に算出されますが、ユーザーによって明示的に上書きされる可能性があります。このフィールドがスキーマに必要な場合でも、フォームに必須にはなることはありません。リスト4-8のように、バリデータを修正してオプションにしている理由はそういうわけです。contentフィールドもサイズを増やして少なくともユーザーが五文字を入力するようにカスタマイズします。

リスト4-8 - バリデータとウィジェットをカスタマイズする

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $this->validatorSchema['slug']->setOption('required', false);
    $this->validatorSchema['content']->setOption('min_length', 5);
 
    $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40));
  }
}

ここではvalidatorSchemawidgetSchemaオブジェクトをPHP配列として使います。これらの配列はフィールドの名前をキーとして取りそれぞれのバリデータオブジェクトと関連するウィジェットオブジェクトを返します。個別のフィールドとウィジェットをカスタマイズできます。

note

オブジェクトをPHP配列として利用できるようにするために、sfValidatorSchemasfWidgetFormSchemaクラスはPHP5以降で利用できるArrayAccessインターフェイスを実装します。

2つの記事が同じslugを持たないことを確認するために、ユニーク制約がスキーマの定義に追加されました。sfValidatorPropelUniqueバリデータを利用することでデータベースレベルでのこの制約はArticleFormフォームに反映されました。このバリデータは任意のフォームフィールドの独自性をチェックできます。例えば、ログインのEメールアドレスの一意性のチェックなどに役立ちます。リスト4-9はArticleFormフォームでこれを使う方法を示しています。

リスト4-9 - フィールドの唯一性をチェックするために sfValidatorPropelUniqueバリデータを使う

class BaseArticleForm extends BaseFormPropel
{
  public function setup()
  {
    // ...
 
    $this->validatorSchema->setPostValidator(
      new sfValidatorPropelUnique(array('model' => 'Article', 'column' => array('slug')))
    );
  }
}

sfValidatorPropelUniqueバリデータはそれぞれのフィールドのバリデーションの後でデータ全体を取り扱うpostValidatorです。slugがユニークであることをバリデートするために、バリデータはslugの値だけでなく主キーの値にもアクセスできなければなりません。スラッグは記事の更新の間は同じ値を保つことができるのでバリデーションルールは作成と編集を通して本当に異なります。

著者がアクティブであるか知るために使われるauthorテーブルのactiveフィールドをカスタマイズしてみましょう。リスト4-10はauthor_idフィールドに接続したsfWidgetPropelSelectウィジェットのcriteriaオプションを修正することでArticleFormフォームからアクティブではない著者を除外する方法を示しています。criteriaオプションはPropelのCriteriaオブジェクトを受け取ることで、ローリングリスト内で利用可能なオプションのリストを絞ることが可能にします。

リスト4-10 - sfWidgetPropelSelectウィジェットをカスタマイズする

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $authorCriteria = new Criteria();
    $authorCriteria->add(AuthorPeer::ACTIVE, true);
 
    $this->widgetSchema['author_id']->setOption('criteria', $authorCriteria);
  }
}

ウィジェットをカスタマイズすることで利用可能なオプションの一覧を絞るることができるとしても、リスト4-11で示されるように、バリデータレベル上でこの範囲の絞り込みを考慮することを忘れてはなりません。sfWidgetProperSelectウィジェットのように、sfValidatorPropelChoiceバリデータはフィールドに対して有効なオプションを絞るためにcriteriaオプションを受け取ります。

リスト4-11 - sfValidatorPropelChoiceバリデータをカスタマイズする

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $authorCriteria = new Criteria();
    $authorCriteria->add(AuthorPeer::ACTIVE, true);
 
    $this->widgetSchema['author_id']->setOption('criteria', $authorCriteria);
    $this->validatorSchema['author_id']->setOption('criteria', $authorCriteria);
  }
}

前の例で、configure()メソッドの中でCriteriaオブジェクトを直接定義しました。我々のプロジェクトにおいて、このcriteriaは他の状況で確実に役立つので、リスト4-12が示すようにAuthorPeerクラスの範囲内でgetActiveAuthorsCriteria()メソッドを作りArticleFormからこのメソッドを呼び出す方がベターです。

リスト4-12 - モデルのCriteriaをリファクタリングする

class AuthorPeer extends BaseAuthorPeer
{
  static public function getActiveAuthorsCriteria()
  {
    $criteria = new Criteria();
    $criteria->add(AuthorPeer::ACTIVE, true);
 
    return $criteria;
  }
}
 
class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $authorCriteria = AuthorPeer::getActiveAuthorsCriteria();
    $this->widgetSchema['author_id']->setOption('criteria', $authorCriteria);
    $this->validatorSchema['author_id']->setOption('criteria', $authorCriteria);
  }
}

tip

sfWidgetPropelSelectウィジェットとsfValidatorPropelChoiceバリデータが2つのテーブル間の一対多のリレーションを表すように、sfWidgetPropelSelectManysfValidatorPropelChoiceManyバリデータは多対多のリレーションを表す同じオプションを受け取ります。ArticleFormフォームにおいて、これらのクラスはarticleテーブルとtagテーブル間のリレーションを表現するために使われます。

バリデータを変更する

スキーマでemailvarchar(255)として定義されたので、symfonyは最大長を255文字に制限するsfValidatorString()バリデータを作りました。このフィールドは妥当なEメールを受け取るようになっており、リスト4-14は生成されたバリデータをsfValidatorEmailバリデータで置き換えています。

リスト4-13 - AuthorFormクラスのemailフィールドバリデータを変更する

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorEmail();
  }
}

バリデータを追加する

前の章で生成されたバリデータを修正する方法を見ました。しかしemailフィールドの場合、これは最大長のバリデーションの維持に役立ちます。リスト4-14において、Eメールの妥当性を保証してフィールドに対して許可された最大長をチェックするためにsfValidatorAndバリデータを使います。

リスト4-14 - 複数のバリデータを使う

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      new sfValidatorString(array('max_length' => 255)),
      new sfValidatorEmail(),
    ));
  }
}

以前の例は完全なものではありません。データベーススキーマでemailフィールドの長さを後で修正することを決める場合、この作業をフォームでも行うことを考慮しなければなりません。生成されたバリデータを置き換える代わりに、リスト4-15で示されるように、バリデータスキーマを追加する方がベターです。

リスト4-15 - バリデータを追加する

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $this->validatorSchema['email'],
      new sfValidatorEmail(),
    ));
  }
}

ウィジェットを変更する

データベーススキーマにおいて、articleテーブルのstatusフィールドは記事のステータスを文字列として保存します。リスト4-16で示されるように、可能な値はArticePeerクラスで定義されました。

リスト4-16 - ArticlePeerクラスで利用可能なステータスを定義する

class ArticlePeer extends BaseArticlePeer
{
  static protected $statuses = array('draft', 'online', 'offline');
 
  static public function getStatuses()
  {
    return self::$statuses;
  }
 
  // ...
}

記事を編集する際に、statusフィールドはテキストフィールドの代わりにドロップダウンのリストとして表現されなければなりません。そのためには、リスト4-17で示されるように、使用したウィジェットを変更してみましょう。

リスト4-17 - statusフィールド用のウィジェットを変更する

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticlePeer::getStatuses()));
  }
}

徹底的にするために、選択されたステータスが実際に利用可能なオプションのリストに所属することを確認するバリデータも変更しなければなりません(リスト4-18)。

リスト4-18 - statusフィールドバリデータを修正する

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $statuses = ArticlePeer::getStatuses();
 
    $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => $statuses));
 
    $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys($statuses)));
  }
}

フィールドを削除する

articleテーブルは2つの特別なカラム、created_atupdated_atを持ち、これらの更新はPropelによって自動的に処理されます。リスト4019で示されるように、ユーザーがこれらを修正できないようにするために、フォームからこれらを削除しなければなりません。

リスト4-19 - フィールドを削除する

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    unset($this->validatorSchema['created_at']);
    unset($this->widgetSchema['created_at']);
 
    unset($this->validatorSchema['updated_at']);
    unset($this->widgetSchema['updated_at']);
  }
}

フィールドを削除するには、バリデータとウィジェットを削除することが必要です。リスト4-20はフォームをPHP配列として使い、1つのアクションで両方を削除することも可能であることを示しています。

リスト4-20 - フォームを利用してPHP配列としてフィールドを削除する

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    unset($this['created_at'], $this['updated_at']);
  }
}

まとめ

まとめるために、リスト4-21とリスト4-22はカスタマイズするArticleFormAuthorFormフォームを示しています。

リスト4-21 - ArticleFormフォーム

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $authorCriteria = AuthorPeer::getActiveAuthorsCriteria();
 
    // ウィジェット
    $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40));
    $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticlePeer::getStatuses()));
    $this->widgetSchema['author_id']->setOption('criteria', $authorCriteria);
 
    // バリデータ
    $this->validatorSchema['slug']->setOption('required', false);
    $this->validatorSchema['content']->setOption('min_length', 5);
    $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys(ArticlePeer::getStatuses())));
    $this->validatorSchema['author_id']->setOption('criteria', $authorCriteria);
 
    unset($this['created_at']);
    unset($this['updated_at']);
  }
}

リスト4-22 - AuthorFormフォーム

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $this->validatorSchema['email'],
      new sfValidatorEmail(),
    ));
  }
}

propel:build-formsを利用することでフォームにオブジェクトモデルをイントロスペクトさせるたいていの要素を自動的に生成できます。この自動処理はいくつかの理由から役立ちます:

  • 退屈で反復される作業をしなくて済むので、開発者の生活が楽になります。プロジェクト固有のビジネスルールに従ってバリデータとウィジェットのカスタマイズに焦点を当てることができます。

  • 加えて、データベーススキーマが更新されたとき、生成フォームは自動的に更新されます。開発者がしなければならないのは行ったカスタマイズの調整だけです。

次のセクションではpropel:generate-crudタスクによって生成されたアクションとテンプレートのカスタマイズ方法を説明します。

フォームのシリアライズ

前のセクションではpropel:build-formsタスクによって生成されたフォームをカスタマイズする方法が示されました。現在のセクションにおいて、propel:generate-crudタスクによって生成されたコードから始めることで、フォームのライフサイクルをカスタマイズします。

デフォルトの値

Propelフォームのインスタンスは常Propelのオブジェクトに接続します。リンクされたPropelオブジェクトは常にgetModelName()メソッドによって返されたクラスに所属します。例えば、AuthorFormフォームはAuthorクラスに所属するオブジェクトのみにリンクできます。このオブジェクトは空のオブジェクト(Authorクラスの空インスタンス)、もしくは最初の引数としてコンストラクタに送り出されるオブジェクトです。"平均的な"フォームのコンストラクタが値の配列を最初の引数として受け取る一方で、PropelフォームのコンストラクタはPropelオブジェクトを受け取ります。このオブジェクトはそれぞれのフィールドのデフォルト値を定義するために使われます。getObject()メソッドは現在のインスタンスに関連するオブジェクトを返しisNew()メソッドによってコンストラクタ経由でオブジェクトが送り出されたかどうかを知ることができます:

// 新しいオブジェクトを作成する
$authorForm = new AuthorForm();
 
print $authorForm->getObject()->getId(); // nullを出力する
print $authorForm->isNew();              // trueを出力する
 
// 既存のオブジェクトを修正する
$author = AuthorPeer::retrieveByPk(1);
$authorForm = new AuthorForm($author);
 
print $authorForm->getObject()->getId(); // 1を出力する
print $authorForm->isNew();              // falseを出力する

ライフサイクルを扱う

章の始めで見たように、リスト4-23で示される、editアクションはフォームのライフサイクルを扱います。

リスト4-23 - authorモジュールのexecuteEditメソッド

// apps/frontend/modules/author/actions/actions.class.php
class authorActions extends sfActions
{
  // ...
 
  public function executeEdit($request)
  {
    $author = AuthorPeer::retrieveByPk($request->getParameter('id'));
    $this->form = new AuthorForm($author);
 
    if ($request->isMethod('post'))
    {
      $this->form->bind($request->getParameter('author'));
      if ($this->form->isValid())
      {
        $author = $this->form->save();
 
        $this->redirect('author/edit?id='.$author->getId());
      }
    }
  }
}

editアクションが以前の章で説明したアクションのような場合でも、わずかな違いを指摘できます:

  • AuthorクラスからのPropelオブジェクトは最初の引数としてフォームコンストラクタに送り出される:

    $author = AuthorPeer::retrieveByPk($request->getParameter('id'));
    $this->form = new AuthorForm($author);
  • 関連テーブル(author)から名付けられたPHP配列の入力データを読み取れるようにウィジェットのname属性フォーマットは自動的にカスタマイズされます:

    $this->form->bind($request->getParameter('author'));
  • フォームが有効なとき、save()メソッドを呼び出すだけではフォームに関連するPropelオブジェクトが作成もしくは更新されます:

    $author = $this->form->save();

Propelオブジェクトを作成して修正する

リスト4-23のコードはAuthorクラスからオブジェクトの作成と修正を行う単独のメソッドを扱います:

  • 新しいAuthorオブジェクトの作成:

    • indexアクションはidパラメータ無しで呼び出されます($request->getParameter('id')null)

    • retrieveByPk()への呼び出しはそれゆえnullを送り出します

    • そしてformオブジェクトはPropelオブジェクトである空のAuthorにリンクされます

    • 有効なフォームが投稿されたとき$this->form->save()を呼び出すことで結果としてAuthorオブジェクトが作成されます

  • 既存のAuthorオブジェクトの修正:

    • indexアクションがidパラメータで呼び出されます(Authorオブジェクトが修正される予定の主キーを表す$request->getParameter('id'))

    • retriveByPk()メソッドの呼び出しは主キーに関連するAuthorオブジェクトを返します

    • formオブジェクトは以前見つかったオブジェクトにリンクされます

    • 有効なフォームが投稿されたとき$this->form->save()の呼び出しによってAuthorオブジェクトが更新されます

save()メソッド

Propelフォームが有効なとき、save()メソッドは関連オブジェクトを更新してデータベースに保存します。実際にはこのメソッドはメインオブジェクトだけでなく潜在的に関連するオブジェクトも保存します。例えば、ArticleFormフォームは記事に結びつけられたタグを更新します。articleテーブルとtagテーブル間の多対多のリレーションでは、記事に関連するタグは(生成されたsaveArticleTagList()メソッドを利用して)article_tagテーブルに保存されます。一貫したシリアライズを保証するために、save()メソッドは1つのトランザクション内ですべての更新を含みます。

note

9章で見るようにsave()メソッドは国際化テーブルも自動的に更新します。

sidebar

bindAndSave()メソッドを利用する

bindAndSave()メソッドはユーザーがフォームに投稿した入力データをバインドし、このフォームをバリデートしてデータベース内で関連するオブジェクト、およびすべてを1つのオペレーションで更新します:

class articleActions extends sfActions
{
  public function executeCreate(sfWebRequest $request)
  {
    $this->form = new ArticleForm();
 
    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('article')))
    {
      $this->redirect('article/created');
    }
  }
}

ファイルのアップロードを扱う

save()メソッドは自動的にPropelオブジェクトを更新しますが、ファイルのアップロードを管理するなどの他の要素を扱うことはできません。

ファイルをそれぞれの記事に添付する方法を見てみましょう。リスト4-24で示されるように、ファイルはweb/uploadsディレクトリに保存されファイルパスへの参照はarticleテーブルのfileフィールドに保持されます。

リスト4-24 - 関連ファイルを持つarticleテーブル用のスキーマ

// config/schema.yml
propel:
  article:
    // ...
    file: varchar(255)

すべてのスキーマを更新した後、オブジェクトモデル、データベースと関連するフォームを更新する必要があります:

$ ./symfony propel:build-all

caution

propel:build-allタスクは再生成するためにすべてのスキーマテーブを削除することを必ず覚えておいてください。それゆえテーブル内のデータは上書きされます。これがそれぞれのモデルを修正時に再びダウンロードするテストデータ(fixtures)を作ることが大切である理由です。

リスト4-25はウィジェットとバリデータをfileフィールドにリンクするためにArticleFormクラスを修正する方法を示します。

リスト4-25 - ArticleFormフォームのfileフィールドを修正する

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $this->widgetSchema['file'] = new sfWidgetFormInputFile();
    $this->validatorSchema['file'] = new sfValidatorFile();
  }
}

ファイルをアップロードするすべてのフォームに関しては、enctype属性をテンプレートのformタグに追加することも忘れないでください(ファイルのアップロード管理に関する詳細な情報は2章を参照してください)。

リスト4-26はファイルをサーバーにアップロードしてパスをarticleオブジェクトに保存するために適用する修正方法を示しています。

リスト4-26 - articleオブジェクトとアップロードされたファイルをアクションに保存する

public function executeEdit($request)
{
  $author = ArticlePeer::retrieveByPk($request->getParameter('id'));
  $this->form = new ArticleForm($author);
 
  if ($request->isMethod('post'))
  {
    $this->form->bind($request->getParameter('article'), $request->getFiles('article'));
    if ($this->form->isValid())
    {
      $file = $this->form->getValue('file');
      $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
      $file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
 
      $article = $this->form->save();
 
      $this->redirect('article/edit?id='.$article->getId());
    }
  }
}

アップロードしたファイルをファイルシステムに保存することでsfValidatedFileオブジェクトはファイルへの絶対パスを知ることができます。save()メソッドを呼び出す間、フィールドの値は関連オブジェクトを更新するために使われ、fileフィールドに関して、__toString()メソッドのおかげでsfValidatedFileオブジェクトは文字列に変換され、ファイルへの絶対パスを送り返します。articleテーブルのfileカラムはこの絶対パスを保存します。

tip

sfConfig::get('sf_upload_dir')ディレクトリへの相対パスを保存したい場合、sfValidatedFileを継承するクラスを作り、 sfValidatorFileバリデータに新しいクラスの名前を送り出すためにvalidated_file_classオプションを使うことが可能です。バリデータはクラスのインスタンスを返します。この章の残りの部分で、オブジェクトをデータベースに保存する前にfileカラムの値を修正する作業で成り立つ別のアプローチを見ることになります。

save()メソッドをカスタマイズする

前の章でアップロードされたファイルをeditアクションで保存する方法を見ました。オブジェクト指向プログラミングの原則の1つはクラスのカプセル化によるコードの再利用です。ArticleFormフォームを利用してそれぞれのアクションでファイルを保存するために使われるコードを重複させるよりも、コードをArticleFormクラスに移動させる方がベターです。リスト4-27はファイルを保存して可能であれば既存のファイルを削除するためにsave()メソッドをオーバーライドする方法を示しています。

リスト4-27 - ArticleFormクラスのsave()メソッドをオーバーライドする

class ArticleForm extends BaseFormPropel
{
  // ...
 
  public function save($con = null)
  {
    if (file_exists($this->getObject()->getFile()))
    {
      unlink($this->getObject()->getFile());
    }
 
    $file = $this->getValue('file');
    $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
    $file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
 
    return parent::save($con);
  }
}

コードをフォームに移動させた後、editアクションはpropel:generate-crudタスクによって初期に生成されたコードにとって理想的なものです。

sidebar

フォームの中でモデルのコードをリファクタリングする

propel:generate-crudタスクによって生成されたアクションを通常は修正すべきではありません。

editアクションに追加できるロジックは、とりわけフォームのシリアライゼーションの間、通常はモデルクラスもしくはフォームクラスに移動させなければなりません。

アップロードされたファイルを考慮するためにフォームクラスでリファクタリングする例を丁度調べました。モデルに関連する別の例を考えましょう。ArticleFormフォームはslugフィールドを持ちます。このフィールドが潜在的にユーザーによって上書きされるtitleフィールドから自動的に計算されるべきであることを理解しました。このロジックはフォームに依存しません。それゆえ、次のコードが示すように、これはモデルに所属します:

class Article extends BaseArticle
{
  public function save($con = null)
  {
    if (!$this->getSlug())
    {
      $this->setSlugFromTitle();
    }
 
    return parent::save($con);
  }
 
  protected function setSlugFromTitle()
  {
    // ...
  }
}

これらのリファクタリングの主要な目的は、アプリケーションレイヤーの分離と、とりわけ開発の再利用性を尊重することです。

doSave()メソッドをカスタマイズする

保存に関連するそれぞれのオペレーションが正しく処理されることを保証するためにオブジェクトの保存はトランザクションの範囲内で行われたことを見ました。アップロードしたファイルを保存するために以前のセクションで行ったようにsave()メソッドをオーバーライドするとき、実行コードはこのトランザクションから独立しています。

リスト4-28はグローバルトランザクションにおいてアップロードしたファイルを保存するコードを挿入するためにdoSave()メソッドを使う方法を示しています。

リスト4-28 - ArticleFormフォームのdoSave()メソッドをオーバーライドする

class ArticleForm extends BaseFormPropel
{
  // ...
 
  protected function doSave($con = null)
  {
    if (file_exists($this->getObject()->getFile()))
    {
      unlink($this->getObject()->getFile());
    }
 
    $file = $this->getValue('file');
    $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
    $file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
 
    return parent::doSave($con);
  }
}

doSave()メソッドはsave()メソッドによって作られたトランザクションの中で呼び出されるので、file()オブジェクトのsave()メソッドを呼び出した際に例外が投げられるとき、オブジェクトは保存されません。

updateObject()メソッドをカスタマイズする

データベースの更新と保存の間にフォームに接続するオブジェクトを修正することが時々必要です。

ファイルのアップロードの例に関しては、アップロードされたファイルへの絶対パスをfileカラムに保存する代わりに、sfConfig::get('sf_upload_dir')ディレクトリへの相対パスを保存したい場合を考えます。

リスト4-29は自動的な更新の後で保存する前にfileカラムの値を作るためにArticleFormフォームのupdateObject()メソッドをオーバーライドする方法を示します。

リスト4-29 - updateObject()メソッドとArticleFormクラスをオーバーライドする

class ArticleForm extends BaseFormPropel
{
  // ...
 
  public function updateObject()
  {
    $object = parent::updateObject();
 
    $object->setFile(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getFile()));
 
    return $object;
  }
}

オブジェクトをデータベースに保存する前にupdateObject()メソッドはdoSave()メソッドによって呼び出されます。

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.