TRAJOIN is an Application to Translate symfony documents Jointly.
home > 1.2/cookbook/en > behaviors.txt
[1] Edit ↑TOPPropelのオブジェクトモデルの2つのクラスに対して同じメソッドを2回書く場合、ビヘイビア(behavior)を考えるときです。既存のメソッドを変更するもしくは新しいメソッドを追加することで、ビヘイビアは同じ方法でいくつかのモデルクラスを拡張するシンプルな方法を提供します。既存のビヘイビアを利用することはとてもシンプルです: symfony bookの8章の関連した部分を読み、それぞれのビヘイビアに投稿されたREADMEファイルに書かれた手引きに従って下さい。しかし新しいビヘイビアを作りたいのであれば、これらの動作方法を理解する必要があります。
ビヘイビアを作るプロセスを説明するために、既に拡張されたモデルで始めます。例えば、セキュリティの理由からarticleテーブルのレコードがデータベースから削除されてはならない場合を想像してみましょう。レコードがArticlePeer::doSelect()へのコールによって返されないように$article->delete()メソッドはレコードをマークしなければなりませんが、内在するデータは削除は削除されてはなりません。これはこのルールを実装するためにPropelモデルを拡張する方法です:
// 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);
}
}
もちろん、deleted_atと呼ばれる新しいタイムスタンプのフィールドをarticleテーブルに追加することを含みます。
: The reason why the method extension applies todoSelectStmt()instead ofdoSelect()is because the former is used not only bydoSelect(), but also bydoCount().
新しいフィールドと変更されたメソッドの組み合わせによってArticleオブジェクトに"paranoid"ビヘイビアが渡します。差し当たり、"ビヘイビア"という言葉はメソッドのセットを参照するだけです。
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.
最初のステップはモデルクラスからコードを取り除き、拡張できるようにフックをそれらに追加することです。
// 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);
}
}
次に、ビヘイビアのコードを新しいコードに追加し、このクラスをオートロードされるディレクトリに設置します:
// 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);
}
}
最後に、新しいParanoidBehaviorクラスのメソッドをArticleとCommentクラスのフックに登録しなければなりません:
// 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'));
mixinの力によって、ビヘイビアのコードはいくつかのモデルオブジェクトをまたがって再利用できます。
しかしフックをモデルクラスに追加しメソッドを登録するタスクは単なるコードのコピーよりもこのプロセスを長くします...。これがsymfonyのビヘイビアが多いなる手助けになるところです。
symfonyはフックをモデルを自動的に追加できます。これらのフックを有効にするために、次のように、propel.iniファイル内で、AddBehaviorsプロパティをtrueに設定します:
propel.builder.AddBehaviors = true // デフォルトの値はfalse
フックが生成されたモデルクラスに挿入されるようにモデルをリビルドする必要があります:
$ php symfony propel-build-model
フックはBaseクラスに追加され、これらのクラスはlib/model/om/ディレクトリの元にあります。例えば、フックが有効である生成されたBaseArticlePeerクラスの抜粋は下記の通りです:
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
}
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.
フックの名前が変更されたので(これはすべて接頭辞がBase)、ステップ3のParanoidビヘイビアのメソッドが登録された方法を変更しなければなりません。それを行う前に、追加されたフックの完全リストを見てみましょう:
// 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
: symfony 1.0に関して、上で説明した4つのフックの代わりに、doSelectメソッドに関連したフックは1つのみです。このことはビヘイビアのいくつかはsymfony 1.1でのみ機能してビヘイビアを完全にはサポートしないsymfony 1.0では機能しないことを説明します。
1つのフック:オブジェクトクラス内で新しいメソッドを許可するものをじっくり見てみましょう。ビヘイビアがpropel.ini内で有効なとき、生成されたすべての基底クラスは下記のコードに似た__call()メソッドを含みます:
// 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);
}
symfony bookの17章で説明したように、__call()に設置されたフックは実行時に可能な新しいメソッドの追加を行います。例えば、deleted_atフラグをリセットできるようにするためにundelete()メソッドをArticleクラスに追加したい場合、これをBehaviorクラスに追加することから始めます:
// In lib/ParanoidBehavior.php
public function undelete($object, $con)
{
$object->setDeletedAt(null);
$object->save($con);
}
それから、次のように新しいメソッドを追加します:
// in config/config.php
sfMixer::register('BaseArticle:undelete', array('ParanoidBehavior', 'undelete'));
// 別のフック
これで$article->undelete() へのすべてのコールは ParanoidBehavior::undelete($article)へのコールとなります。
: 不幸なことに、PHP5に関して、スタティックメソッドのコールは__call()によって捕捉することはできません。このことはsymfonyのビヘイビアは新しいメソッドをPeerクラスに追加できないことを意味します。
ArticleとCommentクラスの両方に対して、Baseフック名を使うために他のフックの登録を書き換える必要もあります。この作業は次のようなものになります:
// 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'));
しかしながらこのコードはあまりD.R.Y.ではありません。それぞれのクラスに対してメソッドのリスト全体を繰り返す必要があるからです。ビヘイビアが何十ダースのメソッドを提供する場合の苦痛を想像して下さい!2つのフェーズで登録処理を分離できればはるかに効率的になります:
symfonyはあなたに変わってこの作業をしてくれる、sfPropelBehaviorと呼ばれるユーティリティクラスを提供します。このクラスを利用するためにステップ3を書き換える方法は下記の通りです:
// 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'));
registerMethodsとregisterHooksのメソッドは両方ともフックのリスト名を最初の引数として求めます。この名前は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の部分は取り除かれました).
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.
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).
To package the behavior into a truly reusable piece of code, the best is to create a plugin.
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'.
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:
plugins/
myPropelParanoidBehaviorPlugin/
lib/
ParanoidBehavior.php // the class containing methods to be mixed in
config/
config.php // the registration of the behavior methods
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.
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.
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.
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.
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:
sfPropelBehavior::add('Article', array('paranoid' => array(
'column' => 'deleted_at'
)));
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:
'propel_behavior_' . [BehaviorName] . '_' . [ClassName] . '_' . [ParameterName]
// in the example above, get the 'deleted_at' value by calling
sfConfig::get('propel_behavior_paranoid_Article_column')
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:
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:
// translateFieldName($name, $origin_format, $dest_format)
// for instance
$name = ArticlePeer::translateFieldName('deleted_at', BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_COLNAME);
So you are now ready to rewrite the ParanoidBehavior class to take the column parameter into account:
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);
}
}
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.
Documentation
| [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) |