AppControllerを分けたときのhelperやcomponentのマージ

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
この記事を書いた時に、AppControllerを複数に分けてるって話をしているんですが、分けるだけならここに書いたようなことで問題ないんですけど、少しだけ頑張らないといけない問題がありまして……。

というのは、ヘルパーやコンポーネントのマージについてです。

例えば、SampleController.phpでだけ使いたいSampleHelperというヘルパーと、全コントローラーで共通で使いたいUtilHelperというヘルパーがあったとしますね。

//SampleController.php
class SampleController extends AppController {
  public $helpers = array('Sample');
}

//AppController.php
class AppController extends Controller {
  public $helpers = array('Util');
}

こうするとCakephpの方で勝手に二つの$helpersをマージしてくれるので、SampleController.phpで呼び出されるページではSampleHelperとUtilHelperの両方が使えるようになります。

ただこのマージを行っているメソッドがですね、簡単に言うとAppControllerを分割するような動きには対応していないようなんです。

なので、対応策をちょっとここで講じてみたいと思っている所存にございます。



Cakephp1.3の頃には_mergeVars()というのがあった

1.3の頃のコアライブラリにあるcontroller.phpというのを見ると、_mergeVars()という関数があります。

中身はこんな感じ。

function __mergeVars() {
  $pluginName = Inflector::camelize($this->plugin);
  $pluginController = $pluginName . 'AppController';

  if (is_subclass_of($this, 'AppController') || is_subclass_of($this, $pluginController)) {
    $appVars = get_class_vars('AppController');
    $uses = $appVars['uses'];
    $merge = array('components', 'helpers');
    $plugin = null;

    if (!empty($this->plugin)) {
      $plugin = $pluginName . '.';
      if (!is_subclass_of($this, $pluginController)) {
        $pluginController = null;
      }
    } else {
      $pluginController = null;
    }

    if ($uses == $this->uses && !empty($this->uses)) {
      if (!in_array($plugin . $this->modelClass, $this->uses)) {
        array_unshift($this->uses, $plugin . $this->modelClass);
      } elseif ($this->uses[0] !== $plugin . $this->modelClass) {
        $this->uses = array_flip($this->uses);
        unset($this->uses[$plugin . $this->modelClass]);
        $this->uses = array_flip($this->uses);
        array_unshift($this->uses, $plugin . $this->modelClass);
      }
    } elseif ($this->uses !== null || $this->uses !== false) {
      $merge[] = 'uses';
    }

    foreach ($merge as $var) {
      if (!empty($appVars[$var]) && is_array($this->{$var})) {
        if ($var === 'components') {
          $normal = Set::normalize($this->{$var});
          $app = Set::normalize($appVars[$var]);
          if ($app !== $normal) {
            $this->{$var} = Set::merge($app, $normal);
          }
        } else {
          $this->{$var} = Set::merge(
            $this->{$var}, array_diff($appVars[$var], $this->{$var})
          );
        }
      }
    }
  }

  if ($pluginController && $pluginName != null) {
    $appVars = get_class_vars($pluginController);
    $uses = $appVars['uses'];
    $merge = array('components', 'helpers');

    if ($this->uses !== null || $this->uses !== false) {
      $merge[] = 'uses';
    }

    foreach ($merge as $var) {
      if (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($this->{$var})) {
        if ($var === 'components') {
          $normal = Set::normalize($this->{$var});
          $app = Set::normalize($appVars[$var]);
          if ($app !== $normal) {
            $this->{$var} = Set::merge($app, $normal);
          }
        } else {
          $this->{$var} = Set::merge(
            $this->{$var}, array_diff($appVars[$var], $this->{$var})
          );
        }
      }
    }
  }
}

難しいことは僕にもよく分かんないんでざっくり言うと、$usesや$helpers、$componentsが各コントローラーとapp_contorller.phpの両方にあったら、それをマージするよってなことです。

注目したいのは、上の5行目と6行目。

if (is_subclass_of($this, 'AppController') || is_subclass_of($this, $pluginController)) {
  $appVars = get_class_vars('AppController');

is_subclass_ofっていうのは、第一引数で指定するオブジェクトの親クラスに第二引数で指定しているクラスがあるかどうかを見る関数です。

つまり、上の場合だと親クラスにAppControllerというクラスがあるかどうかを見ているわけですね。
もしあれば、AppControllerの中で定義しているメンバ変数を取得すると、そんな感じです。

あ、ちなみに$pluginControllerってのは、今日は無視します。俺もよく分かってねーし。

で、取得した値を各コントローラーの値とマージしているのがこの_mergeVars()という関数なわけですが、このソースからも分かる通り、これ、AppControllerしかマージしてくれないようになってるんですね。

今ここで、前の記事同様に、AppControllerを二つに分けたとします。

//各コントローラー
//users_controller.php
class UsersController extends AdminAppController {
    var $helpers = array('Sample');
}

//sample_controller.php
class SampleController extends FrontAppController {
    var $helpers = array('Sample');
}

//AppController
//admin_app_controller.php
class AdminAppController extends AppController {
    var $helpers = array('Util');
}

//front_app_controller.php
class FrontAppController extends AppController {
    var $helpers = array('Util');
}

//app_controller.php
class AppController extends AppController {
    var $helpers = array('Common');
}

まあ、ちょっと雑過ぎますが、例えばこんな感じで各ヘルパーを読み込むようにしたとしましょう。

このとき、マージされるのはUsersControllerやSampleControllerとAppControllerだけなので、使えるヘルパーはSampleHelperとCommonHelperの2つだけになります。UtilHelperはマージされない。



じゃあこれをマージされるようにしてみましょう

controller.phpにある_mergeVars()をapp_controller.phpでオーバーライドします。

そんなに書き換える部分は多くないので、とりあえずコードを書いてみましょう。

function __mergeVars() {
  //AppControllerを配列に入れる
  $this->_mergeParent[] = get_class();

  //$this->_mergeParentに入っているコントローラーの値をマージ
  foreach ($this->_mergeParent as $parent){

    $pluginName = Inflector::camelize($this->plugin);
    $pluginController = $pluginName . $parent;

    if (is_subclass_of($this, $parent) || is_subclass_of($this, $pluginController)) {
      $appVars = get_class_vars($parent);
      $uses = $appVars['uses'];
      $merge = array('components', 'helpers');
      $plugin = null;

      if (!empty($this->plugin)) {
        $plugin = $pluginName . '.';
        if (!is_subclass_of($this, $pluginController)) {
          $pluginController = null;
        }
      } else {
        $pluginController = null;
      }

      if ($uses == $this->uses && !empty($this->uses)) {
        if (!in_array($plugin . $this->modelClass, $this->uses)) {
          array_unshift($this->uses, $plugin . $this->modelClass);
        } elseif ($this->uses[0] !== $plugin . $this->modelClass) {
          $this->uses = array_flip($this->uses);
          unset($this->uses[$plugin . $this->modelClass]);
          $this->uses = array_flip($this->uses);
          array_unshift($this->uses, $plugin . $this->modelClass);
        }
      } elseif ($this->uses !== null || $this->uses !== false) {
        $merge[] = 'uses';
      }

      foreach ($merge as $var) {
        if (!empty($appVars[$var]) && is_array($this->{$var})) {
          if ($var === 'components') {
            $normal = Set::normalize($this->{$var});
            $app = Set::normalize($appVars[$var]);
            if ($app !== $normal) {
              $this->{$var} = Set::merge($app, $normal);
            }
          } else {
            $this->{$var} = Set::merge(
              $this->{$var}, array_diff($appVars[$var], $this->{$var})
            );
          }
        }
      }
    }

    if ($pluginController && $pluginName != null) {
      $appVars = get_class_vars($pluginController);
      $uses = $appVars['uses'];
      $merge = array('components', 'helpers');

      if ($this->uses !== null || $this->uses !== false) {
        $merge[] = 'uses';
      }

      foreach ($merge as $var) {
        if (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($this->{$var})) {
          if ($var === 'components') {
            $normal = Set::normalize($this->{$var});
            $app = Set::normalize($appVars[$var]);
            if ($app !== $normal) {
              $this->{$var} = Set::merge($app, $normal);
            }
          } else {
            $this->{$var} = Set::merge(
              $this->{$var}, array_diff($appVars[$var], $this->{$var})
            );
          }
        }
      }
    }
  }
}

ようはis_subclass_ofで判定するクラスをAppController以外にも対応できるようにすれば良いわけですね。

なので、まずはその受け皿となる変数を用意してやる。『$this->_mergeParent』というのがそれです。

配列にしているのは、マージしたいコントローラーが複数ある場合を想定してのことです。っていうか、こうしとかないと後々よくないことが起こる。起こるし怒る。

あとは、コードの中で『’AppController’』と書かれていた部分をforeach文の要素の変数に置き換える。

上の例だと『$parent』ですね。

これで、AppController以外のコントローラーもマージできる体勢が整いました。

あとは$this->_mergeParentに必要なコントローラーを書く。

class AdminAppController extends AppController {
    var $helpers = array('Util');
    var $_mergeParent = array('AdminAppController');
}

class FrontAppController extends AppController {
    var $helpers = array('Sample');
    var $_mergeParent = array('FrontAppController');
}

こうすると、AdminAppControllerやFrontAppControllerで定義したヘルパーやコンポーネントをマージすることができます。

ただしこの書き方だと、今度はAppControllerがマージされなくなってしまうので、コードの3行目に書いてある『$this->_mergeParent[] = get_class();』で、AppControllerも強制的にマージできるようにしてます。

配列にしているのもこのためっす。


ところで、これはCakephp1.3の頃の話ね。2.0はまたちょっと違ってました。



2.0になったら_mergeVars()がなくなってた

今回2.0にアップグレードしたら、途端にこのマージが行われなくなりまして……ソースを調べたところ、Controller.phpに以前あったはずの_mergeVars()という関数がなくなっておりました。

正確に言うと、Controllerの親クラスであるObjectクラスの中に移動していました。Object.phpを見たら発見した。

その代わり、Controller.phpの中に_mergeControllerVars()という関数がありまして、どうやらこれがヘルパーやコンポーネントのマージをするようになっています。

じゃ、ソースを見てみましょ。

protected function _mergeControllerVars() {
  $pluginController = $pluginDot = null;

  if (!empty($this->plugin)) {
    $pluginController = $this->plugin . 'AppController';
    if (!is_subclass_of($this, $pluginController)) {
      $pluginController = null;
    }
    $pluginDot = $this->plugin . '.';
  }

  if ($pluginController && $this->plugin != null) {
    $merge = array('components', 'helpers');
    $appVars = get_class_vars($pluginController);
    if (
      ($this->uses !== null || $this->uses !== false) &&
      is_array($this->uses) && !empty($appVars['uses'])
    ) {
      $this->uses = array_merge($this->uses, array_diff($appVars['uses'], $this->uses));
    }
    $this->_mergeVars($merge, $pluginController);
  }

  if (is_subclass_of($this, $this->_mergeParent) || !empty($pluginController)) {
    $appVars = get_class_vars($this->_mergeParent);
    $uses = $appVars['uses'];
    $merge = array('components', 'helpers');

    if ($uses == $this->uses && !empty($this->uses)) {
      if (!in_array($pluginDot . $this->modelClass, $this->uses)) {
        array_unshift($this->uses, $pluginDot . $this->modelClass);
      } elseif ($this->uses[0] !== $pluginDot . $this->modelClass) {
        $this->uses = array_flip($this->uses);
        unset($this->uses[$pluginDot . $this->modelClass]);
        $this->uses = array_flip($this->uses);
        array_unshift($this->uses, $pluginDot . $this->modelClass);
      }
    } elseif (
      ($this->uses !== null || $this->uses !== false) &&
      is_array($this->uses) && !empty($appVars['uses'])
    ) {
      $this->uses = array_merge($this->uses, array_diff($appVars['uses'], $this->uses));
    }
    $this->_mergeVars($merge, $this->_mergeParent, true);
  }
}

最終的にはこの関数の中で_mergeVars()を呼び出しているみたいなんですがね。何でこうなったのかは開発者に訊かないと分からないっす。

何にせよ、こんなことになったので従来通りAppControllerで_mergeVars()を上書きしてもダメになってしまいました。

ただ、この_mergeControllerVars()ができて改善された部分は、AppController以外にも対応している部分ですね。

24行目を見てほしいんですけど、is_subclass_ofでチェックしているクラスが『$this->_mergeParent』になってますね。つまりAppController固定ではなく、この変数に入っているクラスをマージできるようになったってことです。

ちなみにこの変数、デフォルトの値はAppControllerになってます。つまり何もしなければAppControllerが勝手にマージされるところは今までと特に変わってないですね。

さて、じゃあここで$this->_mergeParentの値をFrontControllerに変えたとしましょうか。

class SampleController extends FrontAppController {
    public $helpers = array('Sample');
}

class FrontAppController extends AppController {
    public $helpers = array('Util');
    protected $_mergeParent = 'FrontAppController';
}

class AppController extends Controller {
    public $helpers = array('Common');
}

こんな感じですね。ちなみに$_mergeParentというメンバ変数はprotectedな変数らしいです。

こうすると、FrontAppControllerのヘルパーがマージされる……んですが。

上記からも分かるように、ここで定義される$this->_mergeParentは配列ではないので、このままだとFrontAppControllerしかマージされません。つまり、SampleHelperとUtilHelperは読み込まれるけど、CommonHelperは読み込まれないってことですね。

なので、やはりAppController.phpでオーバーライドします。

protected function _mergeControllerVars() {
  $pluginController = $pluginDot = null;

  if (!empty($this->plugin)) {
    $pluginController = $this->plugin . 'AppController';
    if (!is_subclass_of($this, $pluginController)) {
      $pluginController = null;
    }
    $pluginDot = $this->plugin . '.';
  }

  if ($pluginController && $this->plugin != null) {
    $merge = array('components', 'helpers');
    $appVars = get_class_vars($pluginController);
    if (
      ($this->uses !== null || $this->uses !== false) &&
      is_array($this->uses) && !empty($appVars['uses'])
    ) {
      $this->uses = array_merge($this->uses, array_diff($appVars['uses'], $this->uses));
    }
    $this->_mergeVars($merge, $pluginController);
  }

  if(!is_array($this->_mergeParent)) {
    $this->_mergeParent = array($this->_mergeParent);
  }

  //AppControllerをマージする
  $this->_mergeParent[] = get_class();

  foreach($this->_mergeParent as $parent) {
    if (is_subclass_of($this, $parent) || !empty($pluginController)) {
      $appVars = get_class_vars($parent);
      $uses = $appVars['uses'];
      $merge = array('components', 'helpers');

      if ($uses == $this->uses && !empty($this->uses)) {
        if (!in_array($pluginDot . $this->modelClass, $this->uses)) {
          array_unshift($this->uses, $pluginDot . $this->modelClass);
        } elseif ($this->uses[0] !== $pluginDot . $this->modelClass) {
          $this->uses = array_flip($this->uses);
          unset($this->uses[$pluginDot . $this->modelClass]);
          $this->uses = array_flip($this->uses);
          array_unshift($this->uses, $pluginDot . $this->modelClass);
        }
      } elseif (($this->uses !== null || $this->uses !== false) && is_array($this->uses) && !empty($appVars['uses'])) {
        $this->uses = array_merge($this->uses, array_diff($appVars['uses'], $this->uses));
      }
      $this->_mergeVars($merge, $parent, true);
    }
  }
}

やり方としては1.3のときと変わらないですね。$this->_mergeParentを配列にして、AppControllerがマージされるようにするだけです。あとはforeach文で回せばオーケー。




ソースをやたらペタペタ貼っつけたせいで随分と長い記事になってしまいましたね。でもこれで、AppControllerを分割してもヘルパーやコンポーネントのマージができるようになります。

もしできなかったって場合は、この記事が読みにくかったかコピペしているのにどこか上記に間違いがあるのかのいずれかの可能性が高いと思いますので、そんときはご一報ください。

いつでもゲザる準備はしておきますゆえ。
 もしかしたら何か関連しているかも? 
 みんなからのコメント 
2012年10月09日 12:34:04
づや
まだhelperしか試してないですが、
$_mergeParentだけで全部読み込まれてる気がするが。

ちなみに2.2.2です。
2012年10月11日 10:52:49
まっち~
ありゃりゃ、そうでしたか。
もしかしたら、1.3の頃に_margeVars()をオーバーライドしてたコードが残ってから、それが逆に動かない原因になってたのかもですね。

ありがとうございます。もう少しいじってみます。