The symfony Cookbook

Propelのビヘイビアの書き方

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


About

You are currently reading "The symfony Cookbook" which is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.

Master symfony

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Books on symfony

Learn more about symfony with the official guides.
books.sensiolabs.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Chapter Content

概要

基本的な例

ミックスインを使う

モデルフックを自動追加する

新しいメソッドを追加する

フックを単独のステップに登録する

ビヘイビアプラグインのパッケージを作成する

パラメータをビヘイビアに渡す

まとめ

symfony training
Be trained by symfony experts
May 29: Paris (Web Development with Symfony2 - Français)
May 31: Paris (Mastering Symfony2 - Français)
Jun 06: Paris (Introduction to Symfony2 - Français)
Jun 06: Paris (Introduction to Symfony2 - English)
Jun 06: Paris (Going Further with Symfony2 - English)
and more...

Search


powered by google
You are currently browsing "The symfony Cookbook" in Japanese for the 1.0 version - Switch to version: - Switch to language:
Creative Commons License This work is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.
Translation of this work into another language is explicitly allowed.
This version of symfony is not maintained anymore.
If some of your projects still use this version, consider upgrading as soon as possible.

概要

Propelオブジェクトのモデルの2つのクラスに対して同じメソッドを2回書く場合、ビヘイビア(behavior)を考えるときです。既存のメソッドを変更するもしくは新しいメソッドを追加することで、ビヘイビアは同じ方法で複数のモデルクラスを拡張するシンプルな方法を提供します。既存のビヘイビアの使い方はとてもシンプルです: symfony bookの8章の関連した箇所を読み、それぞれのビヘイビアに添付されたREADMEファイルに書かれた手引きに従ってください。しかし新しいビヘイビアを作りたいのであれば、これらの動作方法を理解する必要があります。

基本的な例

ビヘイビアを作る手順を説明するために、既に拡張されたモデルで始めます。たとえば、セキュリティの理由からarticleテーブルのレコードがデータベースから削除されてはならない場合を想像してみましょう。レコードがArticlePeer::doSelect()へのコールによって返されないように$article->delete()メソッドはレコードをマークしなければなりませんが、内在するデータは削除されてはなりません。このルールを実装するためにPropelモデルを拡張する方法は次の通りです:

// lib/model/Article.phpの中で
class Article extends BaseArticle()
{
  public function delete($con = null)
  {
    $this->setDeletedAt(time());
    $this->save($con);
  }
}
 
// lib/model/ArticlePeer.phpの中で
class ArticlePeer extends BaseArticlePeer()
{
  public function doSelectRS(Criteria $criteria, $con = null)
  {
    $criteria->add(self::DELETED_AT, null, Criteria::ISNULL);
 
    return parent::doSelectRS($criteria, $con);
  }
}

もちろん、articleテーブルにdeleted_atと呼ばれる新しいタイムスタンプのフィールドを追加することを含みます。

doSelect()の代わりにdoSelectRS()を適用する理由は前者がdoSelect()だけでなくdoCount()にも使われるからです。

新しいフィールドと変更されたメソッドの組み合わせによってArticleオブジェクトに"paranoid"ビヘイビアを渡します。差し当たり、"ビヘイビア"という言葉はメソッドの一式を指し示します。

ミックスインを使う

では、commentテーブルの削除されたレコードも保持する必要がある場合を想像してみましょう。D.R.Y.ではない、上記の2つのメソッドをCommentCommentPeerクラスにコピーする作業の代わりに、新しいクラスで1回以上使われるコードをリファクタリングして、ミックスインシステムを通して投入します。下記のコードを理解するにはミックスインとsfMixerクラスの概念に慣れていなければなりませんので、これが何を言っているのかわからないのであれば、symfony bookの17章を参照してください。

最初のステップは、拡張できるようにモデルクラスからコードを取り除きそれらにフックを追加することです。

// ステップ1
// lib/model/Article.phpの中で
class Article extends BaseArticle()
{
  public function delete($con = null)
  {
    foreach (sfMixer::getCallables('Article:delete:pre') as $callable)
    {
      $ret = call_user_func($callable, $this, $con);
      if ($ret)
      {
        return;
      }
    }
 
    return parent::delete($con);
}
 
// lib/model/ArticlePeer.phpの中で
class ArticlePeer extends BaseArticlePeer()
{
  public function doSelectRS(Criteria $criteria, $con = null)
  {
    foreach (sfMixer::getCallables('ArticlePeer:doSelectRS:doSelectRS') as $callable)
    {
      call_user_func($callable, 'ArticlePeer', $criteria, $con);
    }
 
    return parent::doSelectRS($criteria, $con);
  }
}
 
// lib/model/Comment.phpの中で
class Comment extends BaseComment()
{
  public function delete($con = null)
  {
    foreach (sfMixer::getCallables('Comment:delete:pre') as $callable)
    {
      $ret = call_user_func($callable, $this, $con);
      if ($ret)
      {
        return;
      }
    }
 
    return parent::delete($con);
}
 
// lib/model/CommentPeer.phpの中で
class CommentPeer extends BaseCommentPeer()
{
  public function doSelectRS(Criteria $criteria, $con = null)
  {
    foreach (sfMixer::getCallables('CommentPeer:doSelectRS:doSelectRS') as $callable)
    {
      call_user_func($callable, 'CommentPeer', $criteria, $con);
    }
 
    return parent::doSelectRS($criteria, $con);
  }
}

次に、新しいコードにビヘイビアのコードを追加し、オートロードされるディレクトリにこのクラスを設置します:

// ステップ 2
// lib/ParanoidBehavior.phpの中で
class ParanoidBehavior
{
  public function preDelete($object, $con)
  {
    $object->setDeletedAt(time());
    $object->save($con);
 
    return true;
  }
 
  public function doSelectRS($class, Criteria $criteria, $con = null)
  {
    $criteria->add(constant("$class::DELETED_AT"), null, Criteria::ISNULL);
  }
}

最後に、ArticleCommentクラスのフックに新しいParanoidBehaviorクラスのメソッドを登録しなければなりません:

// ステップ 3
// config/config.phpの中で
sfMixer::register('Article:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('ArticlePeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));
sfMixer::register('Comment:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('CommentPeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));

ミックスインの力によって、複数のモデルオブジェクトにまたがってビヘイビアのコードを再利用できます。

しかしフックをモデルクラスに追加しメソッドを登録する作業はコードの単なるコピーよりも作業時間が長くなります...。この問題に対してsymfonyのビヘイビアが大きな手助けになります。

モデルフックを自動追加する

symfonyはフックをモデルに自動追加できます。これらのフックを有効にするにに、次のように、propel.iniファイルのAddBehaviorsプロパティをtrueに設定します:

propel.builder.AddBehaviors = true     // デフォルトの値はfalse

生成されたモデルクラスにフックが挿入されるようにモデルをリビルドする必要があります:

$ php symfony propel-build-model

Baseクラスにフックは追加され、これらのクラスはlib/model/om/ディレクトリの中にあります。たとえば、フックが有効であるBaseArticlePeer生成クラスの抜粋内容は下記の通りです:

public static function doSelectRS(Criteria $criteria, $con = null)
{
  foreach (sfMixer::getCallables('BaseArticlePeer:doSelectRS:doSelectRS') as $callable)
  {
    call_user_func($callable, 'BaseArticlePeer', $criteria, $con);
  }
 
  // コードの残り
}

これはステップ1の間にカスタムのArticlePeerに手作業で追加した内容とほとんど同じフックです。違いは登録されたフックの名前がArticlePeer:doSelectRS:doSelectRSの代わりにBaseArticlePeer:doSelectRS:doSelectRSであることです。ですのでステップ1の間に追加されたカスタムクラスを除去できます。このことはビヘイビアがpropel.iniで有効になったとき、もはやフックを手動でモデルクラス内部に追加する必要がないことを意味します。

フックの名前が変更されたので(これはすべて接頭辞がBase)、ステップ3のParanoidビヘイビアのメソッドの登録方法を変更しなければなりません。それを行う前に、追加されたフックの完全なリストを見てみましょう:

// 基底オブジェクトクラスに追加されたフック
[className]:delete:pre     // 削除前
[className]:delete:post    // 削除後
[className]:save:pre       // 保存前
[className]:save:post      // 保存後
[className]:[methodName]   // __call()内部 (新しいメソッドを許可する)
// Peer基底クラスに追加されたフック
[PeerClassName]:doSelectRS:doSelectRS
[PeerClassName]:doSelectJoin:doSelectJoin
[PeerClassName]:doSelectJoinAll:doSelectJoinAll
[PeerClassName]:doSelectJoinAllExcept:doSelectJoinAllExcept
[PeerClassName]:doUpdate:pre
[PeerClassName]:doUpdate:post
[PeerClassName]:doInsert:pre
[PeerClassName]:doInsert:post

symfony 1.0に関して、上で説明した4つのフックの代わりに、doSelectメソッドに関連したフックは1つのみです。このことはビヘイビアの中にはsymfony 1.1でのみ動作して、ビヘイビアのサポートが不完全であるsymfony 1.0では動作しないものがあることの説明になります。

新しいメソッドを追加する

オブジェクトクラスの新しいメソッドを可能にするフックの1つをじっくり見てみましょう。propel.iniでビヘイビアが有効な場合、生成されたすべての基底クラスは下記のコードのような__call()メソッドを含みます:

// lib/model/om/BaseArticle.phpの中で
public function __call($method, $arguments)
{
  if (!$callable = sfMixer::getCallable('BaseArticle:'.$method))
  {
    throw new sfException(sprintf('Call to undefined method BaseArticle::%s', $method));
  }
 
  array_unshift($arguments, $this);
 
  return call_user_func_array($callable, $arguments);
}

symfony bookの17章で説明されているように、__call()に設置されたフックは実行時に利用可能な新しいメソッドを追加します。たとえば、deleted_atフラグをリセットできるようにするためにundelete()メソッドをArticleクラスに追加したい場合、Behaviorクラスに次のようなコードを追加することから始めます:

// lib/ParanoidBehavior.phpの中で
public function undelete($object, $con)
{
  $object->setDeletedAt(null);
  $object->save($con);
}

それから、次のような新しいメソッドを追加します:

// config/config.phpの中で
sfMixer::register('BaseArticle:undelete', array('ParanoidBehavior', 'undelete'));
// 別のフック

これで$article->undelete()へのすべてのコールは ParanoidBehavior::undelete($article)を呼び出します。

不幸なことに、PHP5に関して、スタティックメソッドのコールは__call()によって捕捉できません。このことはsymfonyのビヘイビアはPeerクラスに新しいメソッドを追加できないことを意味します。

フックを単独のステップに登録する

ArticleCommentクラスの両方に対して、Baseフックの名前を使うために他のフックの登録も書き換える必要があります。この作業は次のようなものになります:

// ステップ 3
// config/config.phpの中で
sfMixer::register('BaseArticle:undelete', array('ParanoidBehavior', 'undelete'));
sfMixer::register('BaseArticle:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('BaseArticlePeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));
 
sfMixer::register('BaseComment:undelete', array('ParanoidBehavior', 'undelete'));
sfMixer::register('BaseComment:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('BaseCommentPeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));

しかしながらこのコードはあまりD.R.Y.ではありません。それぞれのクラスに対してメソッドのリスト全体を繰り返す必要があるからです。ビヘイビアが何十ダースのメソッドを提供する場合に感じる苦痛を想像してください!2つのフェーズで登録処理を分離できれば作業ははるかに効率的になります:

  1. ビヘイビアのメソッドをclass-agnosticフックの一覧に登録する。
  2. それぞれのクラスに対して、クラスを認識できない(class-agnostic)フックを実際のフックに変換し、ミックスインシステムを利用してそれらを登録する。

symfonyはあなたに変わってこの作業をしてくれる、sfPropelBehaviorと呼ばれるユーティリティクラスを提供します。このクラスを利用するためにステップ3を書き換える方法は下記の通りです:

// フェーズ 1
// config/config.phpの中で
sfPropelBehavior::registerMethods('paranoid', array(
  array('ParanoidBehavior', 'undelete')
));
sfPropelBehavior::registerHooks('paranoid', array(
  ':delete:pre'                => array('ParanoidBehavior', 'preDelete'),
  'Peer:doSelectRS:doSelectRS' => array('ParanoidBehavior', 'doSelectRS')
));
 
// フェーズ2
// in lib/model/Article.php
sfPropelBehavior::add('Article', array('paranoid'));
 
// lib/model/Comment.phpの中で
sfPropelBehavior::add('Comment', array('paranoid'));

registerMethodsregisterHooksのメソッドは両方ともフックの名前のリストを最初の引数として要求します。モデルクラスにビヘイビアメソッドが追加されるときこの名前はショートカットとして使われます。registerHooksを呼び出すときに使われるフックの名前が特定のモデルの名前への参照を持たないことに注意してください(フックの名前のBaseArticleの一部は取り除かれました。

また、registerMethodsの方法で追加されたメソッドんためにメソッドの名前を指定する必要はありません。ビヘイビアクラスのメソッドの名前がデフォルトで使われます。

sfPropelBehavior::add()ステートメントが実行されるときのみフックは本当のフックの名前でsfMixerクラスに登録されます。このコールの最初のパラメータはモデルクラスなので、sfPropelBehaviorはフックの完全な名前を再現するすべての要素を持ちます(この場合、モデルクラスの名前とビヘイビアのフックの名前にBaseの文字列を連結させる)。

ビヘイビアプラグインのパッケージを作成する

本当に再利用可能なコードのピースとしてビヘイビアのパッケージを作るための最良の方法はプラグインを作ることです。

ビヘイビアプラグインの名前に関して文章で書かれた慣習はありません。これらはこのPropel対してのみ動作するので、これらの接頭辞は'Propel'でなければなりません。またこれらの接尾辞は'BehaviorPlugin'でなければなりません。ですので我々のParanoidビヘイビア用の良い名前は'myPropelParanoidBehaviorPlugin'となります。

今のところ、プラグインに設置するファイルは2つ: ParanoidBehaviorクラス、とビヘイビアメソッドとフックを登録するためにconfig/config.phpに書かれたコードだけです。17章はプラグインのツリー構造でこれらのファイルを編成する方法を説明します:

plugins/
  myPropelParanoidBehaviorPlugin/
    lib/
      ParanoidBehavior.php    // ミックスインされるメソッドを含むクラス
    config/
      config.php              // ビヘイビアメソッドの登録

プロジェクトにインストールされるすべてのプラグインのconfig.phpファイルはそれぞれのリクエストごとに実行されるので、これはビヘイビアのメソッドを登録するための完全な場所です。

プラグイン作成作業を完了させるには、プラグインのrootディレクトリにインストール方法と使い方の手引きを記したREADMEファイルを追加しなければなりません。最良のビヘイビアはユニットテストも含みます。

結局の所、package.xmlを追加し(手動もしくはsfPackageMakerPluginを利用する)、PEARでパッケージを作成すれば、再利用する準備ができています。これをsymfony公式サイトに投稿することもできます。

パラメータをビヘイビアに渡す

良く設計されたビヘイビアは決め打ちされた値に依存しません。上記のParanoidビヘイビアの例の場合、deleted_atカラムが決め打ちされ、パラメータに変換されます。

ビヘイビアにパラメータを渡すには、2番目のパラメータにsfPropelBehavior::add()へのコールとして通常の配列の代わりに連想配列を次のように使います:

sfPropelBehavior::add('Article', array('paranoid' => array(
  'column' => 'deleted_at'
)));

それから、ビヘイビアクラスのこのパラメータの値を取得するには、sfConfigレジストリを使用しなければなりません。パラメータは次の内容で構成されるsfConfigキーに保存されます:

'propel_behavior_' . [BehaviorName] . '_' . [ClassName] . '_' . [ParameterName]
// 上記の例において、次のコードで'deleted_at'の値を取得する
sfConfig::get('propel_behavior_paranoid_Article_column')

問題はビヘイビアメソッドはカラムの名前だけを使うわけではないということです。これらは実現するオペレーションに従ってこれらのさまざまなバージョンの名前を使います:

フォーマットの名前 使われる場所
BasePeer::TYPE_FIELDNAME deleted_at schema.yml
BasePeer::TYPE_PHPNAME DeletedAt メソッドの名前
BasePeer::TYPE_COLNAME DELETED_AT Criteriaパラメータ

ですのでビヘイビアクラスはフィールドの名前に関して特定のフォーマットから別のフォーマットに変換する方法が必要になります。幸いにして、すべてのモデルのPeer生成基底クラスはtranslateFieldName()スタティックメソッドを提供します。この構文はきわめてシンプルです:

// translateFieldName($name, $origin_format, $dest_format) 
// 例
$name = ArticlePeer::translateFieldName('deleted_at', BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_COLNAME);

これでcolumnパラメータを考慮に入れるためにParanoidBehaviorクラスを書き直す準備ができています:

class sfPropelParanoidBehavior
{
  public function preDelete($object, $con = null)
  {
    $class = get_class($object);
    $peerClass = get_class($object->getPeer());
 
    $columnName = sfConfig::get('propel_behavior_paranoid_'.$class.'_column', 'deleted_at');
    $method = 'set'.call_user_func(array($peerClass, 'translateFieldName'), $columnName, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_PHPNAME);
    $object->$method(time());
    $object->save();
 
    return true;
  }
 
  public function doSelectRS($class, $criteria, $con = null)
  {
    $columnName = sfConfig::get('propel_behavior_paranoid_'.$class.'_column', 'deleted_at');
    $criteria->add(call_user_func(array($class, 'translateFieldName'), $columnName, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_COLNAME), null, Criteria::ISNULL);
  }
}

まとめ

Propelのビヘイビアは予め定義されているフックのセットと、単独のステートメントで複数のフックの登録を円滑にするために設計されたヘルパークラスにすぎません。ミックスインを理解していれば、独自のビヘイビアを編集することはそれほど難しくありません。独自のビヘイビアを書き始める前に既存のビヘイビアプラグインを必ず確認してください: これらはビヘイビアの構文の実践的な例です。

Questions & Feedback

If you find a typo or an error, please register and open a ticket.

If you need support or have a technical question, please post to the official user mailing-list.