CakePHP2のClass読み込み

経緯

リファクタのためのクラス分割用にトップディレクトリにsrcなどのディレクトリを作成して、App::build()で登録、App::use()で読み込み使えるようにしたかった。

結論として

無駄にトップディレクトリにこだわっていたけれども、
素直にappディレクトリ以下を使った方が良い。

やったこと

とりあえずまずはトップディレクトリにsrc/Hoge/Fuga.phpを作成。

src/
  Hoge/
    Hoge.php 

適当にテストを書いてHoge.phpに記述したclass Hogeを読み込んでみる。

<?php
class HogeTest {
    
    public function testHoge ()
    {
        App::build(
            [
                'Sample' => [ROOT . DS . 'src' . DS],
            ],
            App::REGISTER
        );

        App::uses('Hoge','Sample');

        // Class Not Found...
        $hoge = new Hoge();
    }
}

Sample/Hoge/Hoge.phpまでは読み込んでくれない。

https://book.cakephp.org/2/ja/core-utility-libraries/app.html#vendor

サブディレクトリがある場合はサブディレクトリも登録してあげる必要があるらしい。

<?php
class HogeTest {
    
    public function testHoge ()
    {
        App::build(
            'Sample' => [
                ROOT . DS . 'src' . DS,
                ROOT . DS . 'src' . DS . 'Hoge' . DS,            
            ],
            App::REGISTER
        );

        App::uses('Hoge','Sample');

        // 読み込めたぞ!
        $hoge = new Hoge();
    }
}

実験は成功だ!!!!!

しかし、リファクタをするにあたりディレクトリが大量にできていくであろう中、
その度にパスを追加する必要があるのか…?
ということに気づいた。

そしてできたコードが以下。

<?php
// ディレクトリを取得する再帰関数
function _load_src($root, &$paths)
{
    $res = glob($root . '/*', GLOB_ONLYDIR | GLOB_MARK);
    if (empty($res)) {
        return;
    }
    $paths = array_merge($paths, $res);
    foreach ($res as $f) {
        _load_src($f, $paths);
    }
}

$_src_paths = [];
_load_src(ROOT . DS . 'src', $_src_paths);

App::build(
    [
        'Sample' => $_src_paths,
    ],
    App::REGISTER
);

これでいいじゃん(いいじゃん)
と思ってbootstrap.phpにこいつをぶち込んでやるぜと意気揚々としていたところ、
そういえばクラス名が競合した場合はどうなるのん?
とふと思ったので試した。

src/
  Hoge.php
  Hoge/
    Hoge.php 
<?php
class Hoge
{
    public function getHoge()
    {
        return 'Hoge';
    }
}
<?php
class Hoge
{
    public function getHoge()
    {
        return 'Fuga';
    }
}

これを出力する。

<?php
/** テストfunction内 **/

// パッケージを追加 ※略
App::uses('Hoge','Sample');
$hoge = new Hoge()
var_dump($hoge->getHoge());
string(4) "hoge"

実験は失敗だ。
考えてみれば当たり前だけれども先に登録されているパスのクラスが使用される。
そのため上記の方法ではsrc以下でクラス名の重複ができない。

つまり

結論に戻って、素直にappディレクトリ以下につくれば良かった。

CakePHP2のクラスロードの仕組みは

<?php

App::use('Hoge','Sample');

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/** classMapに ['Hoge' => 'Sample'] として登録される。 **/
public static function uses($className, $location) {
    self::$_classMap[$className] = $location;
}

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/** ロード処理 **/
public static function load($className) {
    if (!isset(self::$_classMap[$className])) {
        return false;
    }

    $parts = explode('.', self::$_classMap[$className], 2);
    list($plugin, $package) = count($parts) > 1 ? $parts : array(null, current($parts));

    if ($file = self::_mapped($className, $plugin)) {
        return include $file;
    }
    $paths = self::path($package, $plugin);

    if (empty($plugin)) {
        // ここに入ってきてCakeの設定した接頭辞がつけられる(Cake規約)
        $appLibs = empty(self::$_packages['Lib']) ? APPLIBS : current(self::$_packages['Lib']);
        $paths[] = $appLibs . $package . DS;
        $paths[] = APP . $package . DS;
        $paths[] = CAKE . $package . DS;
    } else {
        $pluginPath = self::pluginPath($plugin);
        $paths[] = $pluginPath . 'Lib' . DS . $package . DS;
        $paths[] = $pluginPath . $package . DS;
    }

    $normalizedClassName = str_replace('\\', DS, $className);
    foreach ($paths as $path) {
        $file = $path . $normalizedClassName . '.php';
        if (file_exists($file)) {
            self::_map($file, $className, $plugin);
            return include $file;
        }
    }
    return false;
}

/** $pathsの中身
array(3) {
  [0]=>
  string(29) "/ROOTHOGE/app/Lib/Sample/"
  [1]=>
  string(25) "/ROOTHOGE/app/Sample/"
  [2]=>
  string(30) "/ROOTHOGE/html/lib/Cake/Sample/"
}
**/

よって、appディレクトリ以下に作成すれば下記のような場合

- app
  - src
    - Hoge.php 
    -Hoge
      - Hoge.php 
<?php

App::uses('Hoge','src');
// または
App::uses('Hoge','src/Hoge');

でクラスを使い分けられるようになるのであった。

  • 完 -

補足

一応App.phpのloadに $paths[] = ROOT . DS . $package . DS;
と記述してあげればできるっちゃできるのだけど流石にコアライブラリいじくりたくなくてやめた。

補足 +

これ単純に先にパスを登録しておくか動的に追加するかでしかないなとか
そもそもnamespaceきってできるじゃんとか色々と気づいた。
元々のソースにnamespace入れようとして死んだ経験からnamespace自体無理じゃん〜
って思ってたけど独立してるソースなら普通に読み込めるんだった。