CakePHP2のオートロードを「最近のやり方」で考える

# TL;DR

* 「Composerの利用を前提に」作る場合、CakePHP2アプリケーションでも独自のオートロードから脱却して良いのではないか
* 根幹となるのはComposerのclassmap autoload
* CakePHP2のオートロードを無くすわけではない(できない)ので、共存させつつ恩恵を受ける方法
    * コアコード等の読み込みはCakePHPのクラスローダーに依存させる等

# 狙いと背景
## 狙い
元々、業務上の問題に対するアプローチを模索して得たアイディアです。
狙うのは「テストの信頼性をあげる」ことにあります。

後述する通り、素のCakePHP2の機構では単体テスト実装上のアンチパターンを踏みやすいというリスクがあります。これを改善したい、というのが狙いです。
より正確に言うなら、「テストの質を高める」というよりは、SUT側の挙動の変更によって「テスト実行時とプロダクト側の実行時の状態を近づける」というアプローチです。端的に言えば「テストとしてCI上で実行した時だけ動くコード」を排除したい、というのが取り組みのコアです。

**「プログラマーの責任」を減らして「ミスが減る」という世界** になる訳です。

思考を停止して述べれば、この種の不安は(テストにだけ頼るなら)徹底的経路テストによって成仏させるしか無さそうですが、緩和するだけならSUT側を「easy」に動くようにすることでも可能なのでは・・・という風に整理しております。

## 背景
CakePHP2は「PHPのオートロード[^spl-autoload]以降」「Composer時代より前」に開発されたフレームワークである、という認識からスタートしましょう。
そのために、`App` クラスを用いた独自の機構を実装しています。
-> [App::load](https://api.cakephp.org/2.10/class-App.html#_load)と[bootstrap](https://github.com/cakephp/cakephp/blob/2.10.0/lib/Cake/bootstrap.php#L149)

ざっくりと言えば、この 「`App::load`で探索可能なクラス(パス)を登録する」のが `App::uses()` で、また `App::import()` はuses()に加えて即時的なファイルの読み込みも行う〜というものです。

[^spl-autoload]: 正確に言えば 「SPLのautoload」というニュアンスになります。双方の違いについては[こちら](https://www.php.net/manual/ja/language.oop5.autoload.php)

### namespaceの有るや無しやとオートロード

問題となるのは、**namespaceの利用を前提とした(PSR-4のような)「読み込み」でない場合、オートロードにより意図せぬ副作用がもたらされる可能性が高くなる** という点です。

PHPのオートロードというのは、結局の所「その行を実行した時点で、呼び出されているクラスが存在しなかったら、いい感じにファイルを読み込む」というものになります。掻い摘んで言えば「新しくファイルを `include 'hoge';`する」という機能です。
ちょ〜素朴にコードを示せば

```php
if (!lcass_exists('Hoge')) {
    include 'Hoge.php';
}
```
です。
ここまで簡潔化すると「何が起きそうか」が透けて見えてくるのではないでしょうか。
すなわち、「どっかのファイルの実行時に行われたオートロードによって、その後に処理される別のファイル上でもHogeクラスを使える」という状態が生じます。  

これは、namespace云々やPSR-0/4とは関係なく起きていることです。
例えばCakePHP2において、「`ArticlesController::beforeFilter()`の中で`$this->loadModel('Author');`した場合、 `/articles/create` ページ(action)において 実行される `$this->Article->doSome()`メソッドの中では無条件に `Author`モデルの(static)メソッドを呼び出せる」という事になります。

とりわけ「PHPプログラムの実行単位」という意味でいうと、(CI上など)複数の単体テストを連続で行う場合には「全ての副作用を最後まで引き継ぐ」ということになります。
そうしてアンチパターンである[Erratic Test](http://xunitpatterns.com/Erratic%20Test.html)を誘発し、[Independent](https://qiita.com/hgsgtk/items/00daa278516d1995dac6#tests-should-be-easy-to-run%E5%AE%9F%E8%A1%8C%E3%81%8C%E7%B0%A1%E5%8D%98)であることを侵害されかねません。

では、なぜ「namespaceがしっかりしていると安心感が有る」のでしょうか？
ここではPSR-0か4かの違いは問題になりません。[^psr-0-autoload]
肝要なのは、「利用者(プログラマーではなく、実行者=コンピューター)が、呼び出したい対象を **明確に** 意識できる」という点だと思います。これは実際、すごくシンプルな話です。

例えばHashクラスを使いたい場合(これはCakePHPを使うとほぼ確実に読み込まれ済みですが)を例に考えてみましょう。

```php
// cake2: namespaceなし
Hash::flatten($ar);

// cake3: namespaceあり
\Cake\Utility\Hash::flatten($ar);
```

後者の方が **詳細に**呼び出される事になります。(ファイルの冒頭で `use \Cake\Utility\Hash;` を宣言していても同じ事です。)
また、PSR-4ベースのオートロードであれば、クラスメソッドを呼び出すために`\Cake\Utility\Hash::{$method}` と書いた時点で、未読み込みクラスを解決してくれることになります。
このように、「クラスの存在を意識すれど、クラスを読み込めるかどうかを意識しなくていい」というのがPSR-0/4及びそれをベースとしたオートローダーのもたらす開発体験である、という事ができます。
CakePHP2のオートロードであっても「やりたいこと」は同じですが、そのやり方が(開発者にとって)自然で、シームレスになるのです。

[^psr-0-autoload]: 「PSR-0だから」といっても、オートロードでやるべきことは「区切り文字で区切ってパス(ディレクトリ名+ファイル名)を探し出し、includeする」です。例えばPackagistを漁ったらこんな実装が見つかりました。 https://github.com/Respect/Loader/blob/0.2.1/library/Respect/Loader.php

#### なぜCakePHP2のオートロードは「まどろっこしく」なっているのか

では、何故にこのような「真の意味で勝手に読んでくれる」状態が実現できるのか？あるいは出来ないのか？という疑問があります。

PSR-0/4の方は、クラス名(FQCN)から「どのパスにあるのか」を解決することが出来ます。
他方で、「namespaceを利用していない」場合に、クラス名から「どこのパスに有るファイルか」を特定するのは大変です。

例えば `Bridge` という名前のクラスがあったら、どうでしょうか？suffixのない単数形の名称なので、`Lib` `Utility` そして `Model` 辺りでしょう。このように、どの(サブ)パッケージがあればパスを解決しに行くことが出来ます[^build-path]。

```php
App::uses('Bridge', 'Model');
$brigde = Bridge::build();
```

いかがでしょう、ともすればおまじない的で見覚えのあるコードが「理屈が透けて見える」ようになったのではないでしょうか。
根底にある「使いたい対象であるクラス名」という与えられた情報から取得できる情報が少なすぎる・・という問題に対して、このように「人力で」読み込み対象のクラスファイルについてヒントを与えておこう！というのが、CakePHP2形式のアプローチなのだなと思いました。

[^build-path]: 「どこのディレクトリを探しに行くか？」という情報は、`App::build()`で予め登録している内容が基になります。また、「全く独特な場所から探しに行き、かつAppクラスのクラスマップに登録する」場合には`App::improt()`に適切な引数を渡して利用することになります。
 
# どうするか

改めて話を整理すると、我々が実現したい世界は「意識することが少なくても、ちゃんと動くコードが書ける」というものです。それに関連して、「テストの時だけどうにかなった」を避けたい・・という気持ちを持っています。

PSR-0/4及びそれをベースとしたオートローダーの活用は、そうした状態への到達を支援できているように思います。それを雑に云うなら、「使おうと思ったクラスが呼び出したとおりに存在する」という点に本質を求めらそうです。

ということで、「我々のCakePHP2」の世界において如何にそれを実現するか？を考えます。

## Composerの活用: CakePHP2が標準としていなかった手段が用意されている
CakePHP2は「Composer以前」のフレームワークなので、Comopserに依存しないように作られていました。
今回の話でいうと、ComposerはPHPアプリケーションの開発において2つの「違い」をもたらしている、と言えると思います。


1. PSR-0/4を含め、オートロードの手段を提供している
    * 詳細はこちらを https://github.com/composer/composer/blob/1.9.0/src/Composer/Autoload/ClassLoader.php
2. デプロイ時点で利用される「セットアップ」手順の提供
    * `composer install` のことです！言い方難しい・・

ここまでに「パスの解決」「クラス名とファイルの紐付け」に関するコストについて、何度か言及してきました。
具体的には「ファイナライズ」のような、アプリケーション実行に際しての事前処理です。これを行う(事を前提とした)のを可能とした点がデカいな〜と感じています。

これによって、やろうと思えば「利用されるクラスを全てオートロード可能にしておく」ための処理、コストが高いと言われた「パスの解決をやっておいちゃう」余地が生まれます。

### Composerのautoloadファイル
Composerのオートロードにおける「解決先」の指定(=CakePHP2でいう`App::build()`に当たるような、どの名前(ないしパッケージ)がどこにあるかを登録する処理)はcomposer.jsonにて行われます。
3つの遅延読み込みと1つの即時(事前)読み込みのためのスキーマがあります。

* 遅延読み込み
    1. PSR-0
    2. PSR-4
    3. ClassMap
* 即時読み込み
    1. Files
 
よく使うのはPSR-4だと思いますが、今回は「ClassMap」に着目します。

https://getcomposer.org/doc/04-schema.md#classmap

Comopserがautoloadファイルを吐き出す際に、ココに指定されたパス(ディレクトリ、ファイル名)にあるクラスを読み取って`[$fqcn => $filePath]`という形式の連想配列 = **クラスマップ** を作成します。そうしてできあがるのが、 `autoload_classmap.php` というファイルです。

内容を見ていきましょう。

 公式ドキュメントでは以下のような記述が例示されています。

```json
{
    "autoload": {
        "classmap": ["src/", "lib/", "Something.php"]
    }
}
```
イメージをつかみやすくするために、次のようなファイルを作ってみました。

```sh
$ tree
.
├── Something.php
├── composer.json
├── lib
│   └── Utility.php
└── src
    ├── Entry.php
    └── Model
        └── User.php
```

この状態で `composer dumpautoload` を実行し、生成された(classmapの)オートロードファイルは次のようになります。

```php
// vendor/composer/autoload_classmap.php
<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'App\\Entry' => $baseDir . '/src/Entry.php',
    'App\\Model\\User' => $baseDir . '/src/Model/User.php',
    'NotAnything' => $baseDir . '/Something.php',
    'Something' => $baseDir . '/Something.php',
    'Utility' => $baseDir . '/lib/Utility.php',
);
```
このファイルは `vendor/autoload.php` を起点として読み込まれるものですが、コレさえあれば `App::uses()` のような手間をアプリケーション側に持たせずとも「NotAnythingクラスがどこにあるか分かる」訳です。

```php
// src/Model/User.php
namespace App\Model;

class User
{
}
```
```php
// Something.php
class NotAnything
{
}

class Something
{
}
```

このように、PSR-0/1/4の制約に縛られずに、「名前空間とファイルパスがどうなっていようと、同じファイルに複数のクラスが定義されていようと、構わずに **FQCNとファイルパスのマップを作成する** 」という挙動となります。

#### autoload_classmapの利用コスト
このように「愚直なクラスマップを作成する」機能にておいては、使えば使うほど容易に「クラスマップが肥大化する」という事態を招きます。
単なる連想配列ですが、今まで使い慣れていないものでもあると「パフォーマンス的にどうなのか？」という疑念も湧きます。

基本的には、(PSR-4ベースのオートロードよりも)低コストにクラスのオートロードを実現させるものだと考えて良いはずです。
実際に、Composerの「最適化オプション」である `composer dumpautoload -o` (あるいは `comopser install -o` でも良いですが)のアイディアは、「 **通常であればautoload_psr4.phpに入るような内容も、autoload_classmap.phpに入れてしまおう** 」というものになります。

なので、少なくとも「普段からComopserのパッケージ(PSR-4によるautoload)を使っている」局面においては、「classmapベースのオートロードが遅いか速いか」は気にするようなコストではなさそうに思います。

この話題については、StackOverflow上にいい感じな議論があるので確認してください。
[php \- Why use a PSR\-0 or PSR\-4 autoload in composer if classmap is actually faster? \- Stack Overflow](https://stackoverflow.com/questions/22803419/why-use-a-psr-0-or-psr-4-autoload-in-composer-if-classmap-is-actually-faster/22823995#22823995)
また、この中で紹介されているblogも有用です。
[Patrick Allaert's Blog about Free Software: Benchmarking Composer autoloading, Symfony and eZ Publish 5](http://patrickallaert.blogspot.com/2013/03/benchmarking-composer-autoloading.html)

ということで、結論としては「開発・実装における安心感と細やかなパフォーマンスチューニングによって受けられる恩恵」を天秤にかけることになると思います。その上で、「かなり肥大化しなければ目につく問題にはならなそう」であり、「最終的にはベンチとってみるしかないよね」という事も意識しながら、「どこまでやるか」の落とし所を見つけていくと良さそうです。