5
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

TL;DR

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

狙いと背景

狙い

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

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

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

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

背景

CakePHP2は「PHPのオートロード1以降」「Composer時代より前」に開発されたフレームワークである、という認識からスタートしましょう。
そのために、App クラスを用いた独自の機構を実装しています。
-> App::loadbootstrap

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

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

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

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

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を誘発し、Independentであることを侵害されかねません。

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

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

// 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のオートロードであっても「やりたいこと」は同じですが、そのやり方が(開発者にとって)自然で、シームレスになるのです。

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

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

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

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

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

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

どうするか

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

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

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

Composerの活用: CakePHP2が標準としていなかった手段が用意されている

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

  1. PSR-0/4を含め、オートロードの手段を提供している
  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」に着目します。

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

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

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

{
    "autoload": {
        "classmap": ["src/", "lib/", "Something.php"]
    }
}

イメージをつかみやすくするために、次のようなファイルを作ってみました。

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

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

// 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クラスがどこにあるか分かる」訳です。

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

class User
{
}
// 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
また、この中で紹介されているblogも有用です。
Patrick Allaert's Blog about Free Software: Benchmarking Composer autoloading, Symfony and eZ Publish 5

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


  1. 正確に言えば 「SPLのautoload」というニュアンスになります。双方の違いについてはこちら 

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

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

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
5
Help us understand the problem. What are the problem?