TRAJOIN is an Application to Translate symfony documents Jointly.

home > 1.2/cookbook/en > behaviors.txt

[1] Edit ↑TOP

Propelのビヘイビアを書く方法


[2] Edit ↑TOP

概要


[3] Edit ↑TOP

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


[4] Edit ↑TOP

基本的な例


[5] Edit ↑TOP

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


[6] Edit ↑TOP

// in lib/model/Article.php
class Article extends BaseArticle()
{
  public function delete($con = null)
  {
    $this->setDeletedAt(time());
    $this->save($con);
  }
}

// in lib/model/ArticlePeer.php
class ArticlePeer extends BaseArticlePeer()
{
  public function doSelectStmt(Criteria $criteria, $con = null)
  {
    $criteria->add(self::DELETED_AT, null, Criteria::ISNULL);

    return parent::doSelectStmt($criteria, $con);
  }
}

[7] Edit ↑TOP

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


[8] Edit ↑TOP
: The reason why the method extension applies to doSelectStmt() instead of doSelect() is because the former is used not only by doSelect(), but also by doCount().


[9] Edit ↑TOP

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


[10] Edit ↑TOP

mixinを入力する


[11] Edit ↑TOP

Now, imagine that you need to keep also the deleted records of the comment table. Instead of copying the two methods above in the Comment and CommentPeer classes, which would not be D.R.Y., you should refactor the code used more than once in a new class, and inject it via the Mixins system. You should be familiar with the concept of Mixins and the sfMixer class to understand the following, so refer to Chapter 17 of the symfony 1.0 book if you wonder what this is about.


[12] Edit ↑TOP

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


[13] Edit ↑TOP

// Step 1
// in 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);
}

// in lib/model/ArticlePeer.php
class ArticlePeer extends BaseArticlePeer()
{
  public function doSelectStmt(Criteria $criteria, $con = null)
  {
    foreach (sfMixer::getCallables('ArticlePeer:doSelectStmt:doSelectStmt') as $callable)
    {
      call_user_func($callable, 'ArticlePeer', $criteria, $con);
    }

    return parent::doSelectStmt($criteria, $con);
  }
}

// in 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);
}

// in lib/model/CommentPeer.php
class CommentPeer extends BaseCommentPeer()
{
  public function doSelectStmt(Criteria $criteria, $con = null)
  {
    foreach (sfMixer::getCallables('CommentPeer:doSelectStmt:doSelectStmt') as $callable)
    {
      call_user_func($callable, 'CommentPeer', $criteria, $con);
    }

    return parent::doSelectStmt($criteria, $con);
  }
}

[14] Edit ↑TOP

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


[15] Edit ↑TOP

// Step 2
// In lib/ParanoidBehavior.php
class ParanoidBehavior
{
  public function preDelete($object, $con)
  {
    $object->setDeletedAt(time());
    $object->save($con);

    return true;
  }

  public function doSelectStmt($class, Criteria $criteria, $con = null)
  {
    $criteria->add(constant("$class::DELETED_AT"), null, Criteria::ISNULL);
  }
}

[16] Edit ↑TOP

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


[17] Edit ↑TOP

// Step 3
// in config/config.php
sfMixer::register('Article:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('ArticlePeer:doSelectStmt:doSelectStmt', array('ParanoidBehavior', 'doSelectStmt'));
sfMixer::register('Comment:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('CommentPeer:doSelectStmt:doSelectStmt', array('ParanoidBehavior', 'doSelectStmt'));

[18] Edit ↑TOP

mixinの力によって、ビヘイビアのコードはいくつかのモデルオブジェクトをまたがって再利用できます。


[19] Edit ↑TOP

しかしフックをモデルクラスに追加しメソッドを登録するタスクは単なるコードのコピーよりもこのプロセスを長くします...。これがsymfonyのビヘイビアが多いなる手助けになるところです。


[20] Edit ↑TOP

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


[21] Edit ↑TOP

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


[22] Edit ↑TOP

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

[23] Edit ↑TOP

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


[24] Edit ↑TOP

$ php symfony propel-build-model

[25] Edit ↑TOP

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


[26] Edit ↑TOP

public static function doSelectStmt(Criteria $criteria, $con = null)
{
  foreach (sfMixer::getCallables('BaseArticlePeer:doSelectStmt:doSelectStmt') as $callable)
  {
    call_user_func($callable, 'BaseArticlePeer', $criteria, $con);
  }

  // Rest of the code
}

[27] Edit ↑TOP

That's almost exactly the same hook as the one added by hand to the custom ArticlePeer during step 1. The difference is that the registered hook name is BaseArticlePeer:doSelectStmt:doSelectStmt instead of ArticlePeer:doSelectStmt:doSelectStmt. So you can remove the code added to the custom classes during Step 1. This means that when Behaviors are enabled in the propel.ini, you no longer need to add hooks manually inside your model classes.


[28] Edit ↑TOP

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


[29] Edit ↑TOP
// Hooks added to the base object class
[className]:delete:pre     // before deletion
[className]:delete:post    // after deletion
[className]:save:pre       // before save
[className]:save:post      // after save
[className]:[methodName]   // inside __call() (allows for new methods)
// Hooks added to the base Peer class
[PeerClassName]:doSelectStmt:doSelectStmt
[PeerClassName]:doSelectJoin:doSelectJoin
[PeerClassName]:doSelectJoinAll:doSelectJoinAll
[PeerClassName]:doSelectJoinAllExcept:doSelectJoinAllExcept
[PeerClassName]:doUpdate:pre
[PeerClassName]:doUpdate:post
[PeerClassName]:doInsert:pre
[PeerClassName]:doInsert:post
[PeerClassName]:doCount:doCount

[30] Edit ↑TOP
: symfony 1.0に関して、上で説明した4つのフックの代わりに、doSelectメソッドに関連したフックは1つのみです。このことはビヘイビアのいくつかはsymfony 1.1でのみ機能してビヘイビアを完全にはサポートしないsymfony 1.0では機能しないことを説明します。


[31] Edit ↑TOP

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


[32] Edit ↑TOP

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


[33] Edit ↑TOP

// in 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);
}

[34] Edit ↑TOP

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


[35] Edit ↑TOP

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

[36] Edit ↑TOP

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


[37] Edit ↑TOP

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

[38] Edit ↑TOP

これで$article->undelete() へのすべてのコールは ParanoidBehavior::undelete($article)へのコールとなります。


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


[40] Edit ↑TOP

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


[41] Edit ↑TOP

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


[42] Edit ↑TOP

// Step 3
// in config/config.php
sfMixer::register('BaseArticle:undelete', array('ParanoidBehavior', 'undelete'));
sfMixer::register('BaseArticle:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('BaseArticlePeer:doSelectStmt:doSelectStmt', array('ParanoidBehavior', 'doSelectStmt'));

sfMixer::register('BaseComment:undelete', array('ParanoidBehavior', 'undelete'));
sfMixer::register('BaseComment:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('BaseCommentPeer:doSelectStmt:doSelectStmt', array('ParanoidBehavior', 'doSelectStmt'));

[43] Edit ↑TOP

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


[44] Edit ↑TOP
  1. ビヘイビアのメソッドをclass-agnosticフックの一覧に登録します。
  2. それぞれのクラスに対して、class-agnosticフックを実際のフックに変換してmixinのシステムを利用してそれらを登録します。

[45] Edit ↑TOP

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


[46] Edit ↑TOP

// Phase 1
// in config/config.php
sfPropelBehavior::registerMethods('paranoid', array(
  array('ParanoidBehavior', 'undelete')
));
sfPropelBehavior::registerHooks('paranoid', array(
  ':delete:pre'                => array('ParanoidBehavior', 'preDelete'),
  'Peer:doSelectStmt:doSelectStmt' => array('ParanoidBehavior', 'doSelectStmt')
));

// Phase 2
// in lib/model/Article.php
sfPropelBehavior::add('Article', array('paranoid'));

// in lib/model/Comment.php
sfPropelBehavior::add('Comment', array('paranoid'));

[47] Edit ↑TOP

registerMethodsregisterHooksのメソッドは両方ともフックのリスト名を最初の引数として求めます。この名前はThis name is then used as a shortcut when the behavior methods are added to the model classes. Notice how the hook names used when calling registerHooks don't contain any reference to a specific model class (フック名のBaseArticleの部分は取り除かれました).


[48] Edit ↑TOP

Also, you don't need to specify a method name for the methods added by way of registerMethods. The name of the method in the behavior class is used by default.


[49] Edit ↑TOP

It's only when the sfPropelBehavior::add() statement is executed that hooks are really registered against the sfMixer class... with a real hook name. As the first parameter of this call is a model class name, the sfPropelBehavior has all the elements to recreate the complete hook names (in this case, by concatenating the string Base with the model class name and the behavior hook name).


[50] Edit ↑TOP

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


[51] Edit ↑TOP

To package the behavior into a truly reusable piece of code, the best is to create a plugin.


[52] Edit ↑TOP

There is a non-written convention about behavior plugin names. They must be prefixed with 'Propel' since they work only for this ORM, and they must end with 'BehaviorPlugin'. So a good name for our Paranoid behavior could be 'myPropelParanoidBehaviorPlugin'.


[53] Edit ↑TOP

As of now, there are only two files to put in the plugin: the ParanoidBehavior class, and the code written in config/config.php to register the behavior methods and hooks. Chapter 17 explains how to organize these files in a plugin tree structure:


[54] Edit ↑TOP
plugins/
  myPropelParanoidBehaviorPlugin/
    lib/
      ParanoidBehavior.php    // the class containing methods to be mixed in
    config/
      config.php              // the registration of the behavior methods

[55] Edit ↑TOP

The config.php file of every plugin installed in a project is executed at each request, so this is the perfect place to register behavior methods.


[56] Edit ↑TOP

To complete the plugin, you must add a README file at the root of the plugin's directory, with installation and usage instructions. The best behaviors also bundle unit tests.


[57] Edit ↑TOP

Eventually, add a package.xml (either manually or by way of sfPackageMakerPlugin), package the plugin with PEAR, and you are ready to reuse it. You can also post it in the symfony website.


[58] Edit ↑TOP

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


[59] Edit ↑TOP

A well-designed behavior doesn't rely on hard coded values. In the example of the Paranoid behavior above, the deleted_at column name is hard coded and should be transformed into a parameter.


[60] Edit ↑TOP

To pass a parameter to a behavior, use an associative array as the second parameter of the call to sfPropelBehavior::add() instead of a regular array, as follows:


[61] Edit ↑TOP

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

[62] Edit ↑TOP

Then, to get the value of this parameter in the behavior class, you must use the sfConfig registry. The parameter is stored in a sfConfig key composed like this:


[63] Edit ↑TOP

'propel_behavior_' . [BehaviorName] . '_' . [ClassName] . '_' . [ParameterName]
// in the example above, get the 'deleted_at' value by calling
sfConfig::get('propel_behavior_paranoid_Article_column')

[64] Edit ↑TOP

The problem is that the behavior methods don't use only column names. They use the various versions of these names according to the operation to achieve:


[65] Edit ↑TOP
Format name                 | Example       | Used in
----------------------------|---------------|-----------
`BasePeer::TYPE_FIELDNAME`  | `deleted_at`  | schema.yml
`BasePeer::TYPE_PHPNAME`    | `DeletedAt`   | Method names
`BasePeer::TYPE_COLNAME`    | `DELETED_AT`  | Criteria parameters

So the behavior class will need a way to translate a field name from one format to the other. Fortunately, the generated Base Peer class of every model provides a static translateFieldName() method. Its syntax is quite simple:


[66] Edit ↑TOP

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

[67] Edit ↑TOP

So you are now ready to rewrite the ParanoidBehavior class to take the column parameter into account:


[68] Edit ↑TOP

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

[69] Edit ↑TOP

結論


[70] Edit ↑TOP

Propel Behaviors are nothing more than a set of predefined hooks, and a helper class designed to facilitate the registration of several hooks in a single statement. If you understand Mixins, it shouldn't be too hard to author your own behaviors. Make sure you check the existing behavior plugins before starting your own: they are practical examples of the behaviors syntax.


Comments

Menu

Documentation



Latest Histories

[php] public stat ...
brtRiver(2009-02-09 13:38:34)
Conclusion ---------- ...
brtRiver(2009-01-05 03:24:05)
Passing a parameter to a ...
brtRiver(2009-01-05 03:23:49)
Packaging a behavior into ...
brtRiver(2009-01-05 03:23:26)
Both the `registerMethods ...
brtRiver(2009-01-05 03:23:06)
[php] // Phase 1 ...
brtRiver(2009-01-05 03:22:56)
Symfony provides a utilit ...
brtRiver(2009-01-05 03:22:38)
1. Register methods of th ...
brtRiver(2009-01-05 03:22:28)
But this code is not very ...
brtRiver(2009-01-05 03:22:13)
[php] // Step 3 ...
brtRiver(2009-01-05 03:22:02)
You still need to rewrite ...
brtRiver(2009-01-05 03:21:43)
Register hooks in a singl ...
brtRiver(2009-01-05 03:21:32)
>**Note**: Unfortunate ...
brtRiver(2009-01-05 03:21:21)
Now, every call to `$arti ...
brtRiver(2009-01-05 03:21:11)
[php] // in confi ...
brtRiver(2009-01-05 03:21:00)
Then, register the new me ...
brtRiver(2009-01-05 03:20:46)
[php] // In lib/P ...
brtRiver(2009-01-05 03:20:33)
As explained in [Chapter ...
brtRiver(2009-01-05 03:20:14)
[php] // in lib/m ...
brtRiver(2009-01-05 03:20:04)
One of the hooks should g ...
brtRiver(2009-01-05 03:19:46)

Untranslated