CakePHPで動的にBehaviorを切り替えてModelの振舞いを変えるときはunload、loadを使う
CakePHP DocumentのBehaviorsの項にも書いてありますが、「ある時はこういう処理をして欲しいけど、違う時は別の処理をして欲しい」みたいな話。
やりたいこと
- あるModelがあって、今はAという処理だけあれば良いのだけど、今後BやCという処理も増やしたい
- 例えば「自動販売機」というModelがあって、今は「缶を売る」という処理だけだけど、今後「ビンを売る」「ペットボトルを売る」という処理も増やしたい
こんな時はデザインパターンのStrategy パターンが使えそうなのだけど、CakePHPでやる時にはBehaviorを使えば似たようなことできるのかな?と思って調べて試してみた。
CakePHPのドキュメントを確認
Behaviorsのドキュメントを読むと、
However, we may need to “detach” behaviors from our models at runtime. Let’s say that on our previous Category model, which is acting as a Tree and a Translate model, we need for some reason to force it to stop acting as a Translate model:
<?php // Detach a behavior from our model: $this->Category->Behaviors->unload('Translate');That will make our Category model stop behaving as a Translate model from thereon.
「実行時にmodelからbehaviorをdetachする必要があったらunloadすればその振舞いは止まるよ」と書いてある。
そして、
Just as we could completely detach a behavior from a model at runtime, we can also attach new behaviors. Say that our familiar Category model needs to start behaving as a Christmas model, but only on Christmas day:
<?php // If today is Dec 25 if (date('m/d') == '12/25') { // Our model needs to behave as a Christmas model $this->Category->Behaviors->load('Christmas'); }We can also use the load method to override behavior settings:
<?php // We will change one setting from our already attached behavior $this->Category->Behaviors->load('Tree', array('left' => 'new_left_node'));
「loadを使えば新しいbehaviorをattachすることもできて、設定も渡せるよ。」と書いてある。
なるほどなるほど。
ということで、具体的に試してみる
単純な例で試したかったので、Head Firstデザインパターンの1章にあるDuckの例(ModelDuck)を参考にした。
登場人物
- Duckクラス
- 色んな鴨の親クラスになる抽象クラス。
- ModelDuckクラス
- 鴨の実装。模型の鴨。飛ぶ手段がないためdefaultでは飛べず、performFly()すると「飛べません」と返す。
- FlyNoWayBehaviorクラス
- performFlyの振舞いの実装。fly()すると「飛べません」と返す。
- FlyRocketPoweredBehaviorクラス
- performFlyの振舞いの実装。fly()すると「ロケットで飛んでいます!」と返す。
- ModelDuckTestクラス
- 確認するためのテストクラス。
ソースコード
まずはテストコード。
基本はbakeで作られたテンプレートなので、「testModelDuckは飛ぶ振舞いが変わる」というmethodの所だけ実装。
やってることは、
- 初期は「飛べない」
- 「飛べない」振舞いをunloadし、「ロケットで飛べる」振舞いをloadし、振舞いを変える
- そうすると「ロケットで飛べる」
という確認だけ。このテストが通れば今回やりたいことはOK。
<?php // app/Test/Case/Model/ModelDuckTest.php App::uses('ModelDuck', 'Model'); class ModelDuckTestCase extends CakeTestCase { public $fixtures = array( ); public function setUp() { parent::setUp(); $this->ModelDuck = ClassRegistry::init('ModelDuck'); } public function tearDown() { unset($this->ModelDuck); parent::tearDown(); } public function testModelDuckは飛ぶ振舞いが変わる() { debug(__FUNCTION__); $this->_modelShouldNotFly(); $this->_changeBehavior(); $this->_modelShouldFly(); } private function _changeBehavior() { $this->ModelDuck->Behaviors->unload('FlyNoWay'); $this->ModelDuck->Behaviors->load('FlyRocketPowered'); } private function _modelShouldNotFly() { $this->assertEquals('飛べません', $this->ModelDuck->performFly(), '飛べない状態になっているか'); } private function _modelShouldFly() { $this->assertEquals('ロケットで飛んでいます!', $this->ModelDuck->performFly(), 'ロケットで飛べているか'); } }
次に具体的な鴨の親にあたるDuckクラス。abstrat。
fly()を呼ぶためのperformFly()が定義されている。具象の鴨を実装する時にactsAsまたはloadでBehaviorを読み込み、fly()の振舞いが決まる。
<?php // app/Model/Duck.php abstract class Duck extends AppModel { public function performFly() { return $this->fly(); } }
今回の具象クラスであるModelDuckクラス。
CakePHPの都合だけども、今回はDBを使わないのでuseTableはfalse。
あとは自分のfly()のdefault振舞いをFlyNoWay(飛べません)にする。
<?php // app/Model/ModelDuck.php App::uses('Duck', 'Model'); class ModelDuck extends Duck { public $useTable = false; public $actsAs = array('FlyNoWay'); public function display() { return '模型の鴨です'; } }
「飛べない」振舞いを表すクラス。
setupはactsAsに指定した時やloadを実行した時に呼ばれる初期値。CakePHPのdefaultのままなので今回は特に何もしてない。
<?php // app/Model/Behavior/FlyNoWayBehavior.php class FlyNoWayBehavior extends ModelBehavior { public function setup(Model $Model, $settings) { if (!isset($this->settings[$Model->alias])) { $this->settings[$Model->alias] = array( 'option1_key' => 'option1_default_value', ); } $this->settings[$Model->alias] = array_merge( $this->settings[$Model->alias], (array)$settings ); } public function fly(Model $model) { return '飛べません'; } }
「飛べない」振舞いを表すクラス。
setUpはFlyNoWayBehavior同様。
<?php // app/Model/Behavior/FlyRocketPoweredBehavior.php class FlyRocketPoweredBehavior extends ModelBehavior { public function setup(Model $Model, $settings) { if (!isset($this->settings[$Model->alias])) { $this->settings[$Model->alias] = array( 'option1_key' => 'option1_default_value', ); } $this->settings[$Model->alias] = array_merge( $this->settings[$Model->alias], (array)$settings ); } public function fly(Model $model) { return 'ロケットで飛んでいます!'; } }
テストの実行
実行すると下のように上手くいきました。
% ./cakephp/lib/Cake/Console/cake -app app testsuite --tap app Model/ModelDuck Welcome to CakePHP v2.0.4 Console --------------------------------------------------------------- App : app Path: /Applications/MAMP/htdocs/lib/app/ --------------------------------------------------------------- CakePHP Test Shell --------------------------------------------------------------- TAP version 13 ok 1 - ModelDuckTestCase::testModelDuckは飛ぶ振舞いが変わる 1..1
まとめ
CakePHPでStrategyパターンみたいなことがやりたいときにはBehaviorのunloadとloadを使えば良さそう。
というか、それがBehaviorの元々の意図なんだろうけども。僕は今までそういう使い方してなかったので。