ControllerからModelを使用するいくつかの方法 (CakePHP Advent Calendar 19日目)

CakePHP Advent Calendar 2010 はみなさんのノウハウがいろいろと知ることができてほんと楽しいですね。

18日目のhaltさんのauthkittenプラグインで子猫認証 (CakePHP Advent Calendar 2010 18日目) を引き継いで、19日の記事となります。


※本日の記事は CakePHP 1.3.6 がベースになっています。

ControllerからModelを使用する方法ですが、一番手軽なのはControllerの「usesプロパティ」だと思います。

usesプロパティは以下の流れでモデルをロードします。

  • Dispatcher#dispatch (webroot/index.php)
  • Dispatcher#_invoke (cake/dispatcher.php)
  • Controller#constuctClasses (cake/libs/controller/controller.php)

Controller#constructClasses のソースは以下のようになっています。

function constructClasses() {
  $this->__mergeVars();
  $this->Component->init($this);

  if ($this->uses !== null || ($this->uses !== array())) {
    if (empty($this->passedArgs) || !isset($this->passedArgs['0'])) {
      $id = false;
    } else {
      $id = $this->passedArgs['0'];
    }

    if ($this->uses === false) {
      $this->loadModel($this->modelClass, $id);
    } elseif ($this->uses) {
      $uses = is_array($this->uses) ? $this->uses : array($this->uses);
      $modelClassName = $uses[0];
      if (strpos($uses[0], '.') !== false) {
        list($plugin, $modelClassName) = explode('.', $uses[0]);
      }
      $this->modelClass = $modelClassName;
      foreach ($uses as $modelClass) {
        $this->loadModel($modelClass);
      }
    }
  }
  return true;
}

最初のif文をよく見ると「!==」でチェックをしていて、Contrrolerでモデルをロードしたくなければ、厳密に「null」か「array()」でなければならないということがわかります。(モデルを使用しないつもりで false を代入してもダメなのはこのせいです)

usesプロパティが false(usesプロパティを指定してない場合も false になる) だと $this->modelClass を使用してモデルをロードします。($this->modelClass はデフォルトでは Controller 名から作られるもので UsersController なら User になるといった規約になります)

usesプロパティに対して配列指定でモデル指定した場合、1つめに設定した値を特別扱いし、$this->modelClassを上書きしています。複数指定する場合、適当に指定していいのではなくて、1つめは特別な意味を持つことを覚えておく必要があります。

さて、モデルのロードで使用されている Controller#loadModel ですが、これはフレームワーク内部だけで使われるものではなくて、我々が普段作成しているControllerの中でモデルをロードしたいときに使えるメソッドになります。(loadModelはCakeBookの「3.5.4.4.7 loadModel」にきちんと説明されています)

class FooController extends AppController {
  public function index()
  {
   // これで $this->Bar->... という形でモデルが使用できる
    $this->loadModel('Bar');
  }
}

Controllerで複数モデルを使用するときにusesに指定してしまうと、モデルを使用しないActionでも全てのモデルをロードしてしまうので、usesに指定するのは全Actionで使用するものだけにし、それ以外は loadModel でロードするのが効率がいいということになります。

Controller#loadModel のソースを見ると、ClassRegistry#init を使用してモデルクラスを生成しています。ClassRegistry#init 内でモデルクラスを生成しているソースの部分を抜き出してみます。(内部的にいろんなことをやってくれてるんですが、今回は説明に必要な部分だけ抜き出します。ClassRegistryに関しては id:hiromi2424 さんの「ClassRegistry徹底解剖」が参考になります)

  if (class_exists($class) || App::import($type, $pluginPath . $class)) {
    ${$class} =& new $class($settings);
  } elseif ($type === 'Model') {
    if ($plugin && class_exists($plugin . 'AppModel')) {
      $appModel = $plugin . 'AppModel';
    } else {
      $appModel = 'AppModel';
    }
    $settings['name'] = $class;
    ${$class} =& new $appModel($settings);
  }

ここで重要になってくるのが elseif の方で、うまくクラス生成ができなくて、さらにそれが Model だった場合、AppModel クラスとして生成されるということです。

usesプロパティに存在しないモデルを指定('User'と指定しないといけないのに'Users'にしてたとか)して、バリデーションがうまく動かずに困ったことがある人は結構いると思うんですが、それはここで AppModel が生成されるからになります。

ということで Controllerでモデルを使用する場合の注意点をまとめると...

  • uses プロパティには必要最低限のモデルを指定する
  • 追加でモデルをロードするには loadModel を使う
  • モデルを使用しない場合は null か array()
  • uses プロパティに存在しないモデルを指定すると AppModel になる

Modelのロードはそれなりに重たい処理なので、不必要なモデルは極力ロードしないように気をつけましょう!

ということで、明日の担当は connvoi_tyou さんです。よろしくお願いします!