フィルタチェーンが気持ち悪いのでActiveRecordをオーバーライドする

Akelos使ってるとバリデータの流れが気持ち悪いと思うことがある。
ActiveRecordでsaveを行うと順々にコールバックメソッドを評価してレコードが登録されるわけだが、メソッド名から想像する順序と実際に実行される順序が違いすぎて困る。

ちなみにソースコード中のコメントには以下のような感じでコールバックメソッドの実行順序が記述されている。

# * - (-) save()
# * - (-) needsValidation()
# * - (1) beforeValidation()
# * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate()
# * - (-) validate()
# * - (-) validateOnCreate()
# * - (4) afterValidation()
# * - (5) afterValidationOnCreate() / afterValidationOnUpdate()
# * - (6) beforeSave()
# * - (7) beforeCreate() / beforeUpdate()
# * - (-) create()
# * - (8) afterCreate() / afterUpdate()
# * - (9) afterSave()
# * - (10) afterDestroy()
# * - (11) beforeDestroy()

想像してた流れはこのコメントのとおりの順番で、この通り動いてくれれば何ら問題はないんだけど、ざっと実際の流れを追ってみると以下のような感じになっている。

+ ---------save()-------------------------------------------------------------+
|                                                                             |
|     (1) beforeSave()  → false                                              |
|           ↓ true                                                           |
| +--------createOrUpdate() ---------------------------------------+          |
| |                                                                |          |
| | +------isValid() -----------------------------------+          | → false |
| | |                                                   |          |          |
| | |  (2) beforeValidation()   → false                | → false |          |
| | |       ↓ true                                     |          |          |
| | |      validate()                                   |          |          |
| | |                                                   |          |          |
| | |  (3) afterValidation()                            |          |          |
| | |                                                   |          |          |
| | |  (4) beforeValidationOn[Create|Update]() → false |          |          |
| | |       ↓ true                                     |          |          |
| | |      validateOn[Create|Update]()                  |          |          |
| | |                                                   |          |          |
| | |  (5) afterValidationOn[Create|Update]()           |          |          |
| | +---------------------------------------------------+          |          |
| |         ↓ true                                                |          |
| | +--- _create() or _update() ---------------+                   |          |
| | |                                          | → false          |          |
| | |  (6) before[Create|Update]() → false    |                   |          |
| | |       ↓ true                            |                   |          |
| | |  (7) after[Create|Update]()  → false    |                   |          |
| | +------------------------------------------+                   |          |
| +----------------------------------------------------------------+          |
|           ↓ true                                                           |
|      (8) afterSave()   → false                                             |
+-----------↓----------------------------------------------------------------+
          [commit]

見てのとおり気持ち悪い。とりあえずafterとかbeforeとか名前つけてるんならちゃんと順番通りやってほしい。
ちなみにsaveと並んでレコード操作によく使われるcreateとupdateについては内部でsaveを呼び出しているため基本的にこの流れになるが、createOnUpdateでレコードの操作を行うとbeforeSaveとafterSaveは通ってくれない。呼ばれたくないならprivateっぽく書けばいいのに。

ついでにafterValidation[Update|Create]についてはfalseを返してもチェーンは中断されない。
これは意図したものかどうかわからないが、他のメソッドと挙動がちがうのはちょっと止めてほしい。

しかも、致命的なことにbeforeValidationでfalseが返るとisValidはtrueを返すためvalidateはスルーされてそのまま登録処理に走るというよく分からない状態になっている。

とにかくこれらをしっかり把握しておかないと、本来便利であるメソッドチェーンがバグの温床になったりするので要注意。

とはいえ、どうみてもバグっぽい仕様なので素直にこの流れに沿ってロジックを書いてると、いつの間にか本家で修正されて涙目という状況になる可能性が高い。

結局shared_model.phpに書くメソッドをオーバーライドしてしまうのが一番手っ取り早い気がしたので勢いでやってしまった。後悔はしていない。

ということで、save,createOrUpdate,isValidをオーバーライドしたshared_model.phpです。

<?php

require_once(AK_LIB_DIR.DS.'AkActiveRecord.php');

/**
* This file is application-wide model file. You can put all 
* application-wide model-related methods here.
* 
* Add your application-wide methods in the class below, your models
* will inherit them.
*
* @package ActiveRecord
* @subpackage Base
*/
class ActiveRecord extends AkActiveRecord 
{
   
   function save($validate = true)
   {
       return $this->createOrUpdate($validate);
   }
   
   function createOrUpdate($validate = true)
   {
       if($this->isFrozen()){
           return false;
       }
       
       if($validate && !$this->isValid()){
           $this->transactionFail();
           return false;
       }
       
       $this->transactionStart();
       
       while(true)
       {
           if(!$this->beforeSave() || !$this->notifyObservers('beforeSave')) break;
         
           $result = $this->isNewRecord() ? $this->_create() : $this->_update();
         
           if($result && !$this->transactionHasFailed() && $this->afterSave() && $this->notifyObservers('afterSave')){
                $this->transactionComplete();
                return true;
           }
           break;
       }
       $this->transactionFail();
       return false;
   }
   
   function isValid()
   {
        $this->clearErrors();
        
        while(true)
        {
          if ($this->isNewRecord()){
              $beforeValidation = 'beforeValidationOnCreate';
              $afterValidation  = 'afterValidationOnCreate';
              $validate = 'validateOnCreate';
          }else{
              $beforeValidation = 'beforeValidationOnUpdate';
              $afterValidation  = 'afterValidationOnUpdate';
              $validate = 'validateOnUpdate';
          }
          
          if(!$this->beforeValidation() || !$this->notifyObservers('beforeValidation')) break;
          if(!$this->$beforeValidation() || !$this->notifyObservers($beforeValidation)) break;
          
          if($this->_set_default_attribute_values_automatically){
              //$this->_setDefaultAttributeValuesAutomatically();
          }
          
          $this->validate();
          $this->$validate();
          
          if($this->_automated_validators_enabled){
              //$this->_runAutomatedValidators();
          }
          
          if(!$this->afterValidation() || !$this->notifyObservers('afterValidation')) break;
          if(!$this->$afterValidation() || !$this->notifyObservers($afterValidation)) break;
          
          return !$this->hasErrors();
        }
        return false;
    }
}

?>

一応メソッドチェーンは期待通り動くし、それぞれでfalseが返ればしっかり中断してくれるようになった。
本家のアップデートを追いかけつつ微妙に修正は入れていかないといけないけど、精神的にはこっちの方がうれしい。