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

Propelのビヘイビアの書き方

1.2
Symfony version Language

概要

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と呼ばれる新しいタイムスタンプのフィールドを追加することを含みます。

note

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

note

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)を呼び出します。

note

不幸なことに、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ファイルはそれぞれのリクエストごとに実行されるので、これはビヘイビアのメソッドを登録するための完全な場所です。

プラグイン作成作業を完了させるには、プラグインのルートディレクトリにインストール方法と使い方の手引きを記した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のビヘイビアは予め定義されているフックのセットと、単独のステートメントで複数のフックの登録を円滑にするために設計されたヘルパークラスにすぎません。 ミックスインを理解していれば、独自のビヘイビアを編集することはそれほど難しくありません。 独自のビヘイビアを書き始める前に既存のビヘイビアプラグインをかならず確認してください: これらはビヘイビアの構文の実践例です。