この記事でできるようになること
【第一回】 autoloadの概要を理解
【第二回】 仕組みを理解したうえでautoloadを実装
【第三回】 composerからautoloadを利用
前回の復習
アジェンダ
- 名前空間がどのような役割を果たすか理解
- PSR-4で使用される用語を理解
- Class Exampleを理解/実装
今回の最終目標はClass Exampleの実装であり、メインテーマは名前空間です。
名前空間を利用し、どんな階層からでもファイルを読み込めるようAutoload
クラスを拡張していきます。
Class Example の解読
下準備
前回と同じファイルを用いて手を動かしながら理解していきましょう。
今回は別々のディレクトリにあるHello.php
をindex.php
から読み込むことを目指します。
./Qiita
├ App
│ └ Greeting
│ ├ English
│ │ └ Hello.php
│ └ Japanese
│ └ Hello.php
├ Autoload.php
└ index.php
<?php
namespace App\Greeting\English;
class Hello
{
public function __construct()
{
echo "Hello World!\n";
}
}
<?php
namespace App\Greeting\Japanese;
class Hello
{
public function __construct()
{
echo "こんにちは世界。\n";
}
}
まずは、Autoload
なしで各Hello
クラスを呼び出してみましょう。
<?php
require_once "App/Greeting/English/Hello.php";
require_once "App/Greeting/Japanese/Hello.php";
use App\Greeting\English as En;
use App\Greeting\Japanese as Ja;
$en = new En\Hello();
$ja = new Ja\Hello();
\Qiita> php index.php
Hello World!
こんにちは世界。
問題なく出力できたかと思います。
完全修飾クラス名
次に以下のコードをindex.php
に追記して実行してみてください。
var_dump(En\Hello::class);
string(26) "App\Greeting\English\Hello"
::class
を用いると名前空間とクラス名をくっつけた文字列が取得できると思います。
これが第一回でちらっと登場した完全修飾クラス名であり、本記事の主人公です。
callback()の引数$classNameには呼び出された未定義クラスの完全修飾クラス名(後に解説)が入る。今は「クラス名が取得できるんだな」程度の理解でOK。
察しのいい方はここで気づくと思いますが、完全修飾クラス名がディレクトリ階層とマッチしています。このルールはPSR-4ではっきりと定義されています。
The contiguous sub-namespace names after the "namespace prefix" correspond to a subdirectory within a "base directory", in which the namespace separators represent directory separators. The subdirectory name MUST match the case of the sub-namespace names.
【問】ファイルディレクトリを求めよ
【解答】
【解説】
紐づけ
Autoloadクラスの最後のチャンスで取得した完全修飾クラス名から、どうにかこうにかしてファイルパスを生成する方法を考えます。
また、どんな階層からでもファイルを読み込めることが条件なので、相対パスではなく絶対パスを生成することを考えます。
絶対パスを生成するということはQiita
ディレクトリ以前のパスが必要になってきます。
例えば、今回私の環境ではQiita
ディレクトリは以下のような場所に置かれています。
C:/Users/yanmy/OneDrive/Desktop/Qiita
ここで問題なのが、このパスが完全修飾クラス名から取得不可能ということです。
完全修飾クラス名はあくまでも名前空間とクラス名をくっつけた文字列です。
そこで、事前に紐づけを行います。
具体的には名前空間先頭の一部(今回はApp
)を「キー」、先ほどのパスを「値」として保存できる連想配列$prefixes
プロパティをAutoload
クラスに作成します。
<?php
class Autoload{
protected $prefixes = array(); //この行を追記
次に紐づけ登録を行うメソッドaddNamespace()
を作成。(一部割愛してます)
//callback()のあとに以下を追記
public function addNamespace($prefix, $base_dir)
{
//あとで使いやすいよう入力値を整形
$prefix = trim($prefix, '\\') . '\\';
$base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
//すでに登録済みのキーか確認
if (isset($this->prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = array();
}
//ここで登録
array_push($this->prefixes[$prefix], $base_dir);
}
処理の内容を細かくは追いませんが、以下を実行することで$prefixes
プロパティに紐づけした内容が登録されます。
$loader = new Autoload();
$loader->addNamespace('App', 'C:/Users/yanmy/OneDrive/Desktop/Qiita/App')
ちなみに登録した名前空間の一部を名前空間プレフィックスと呼び、
パスをベースディレクトリと呼びます。別に美味しくはないです。
名前空間プレフィックスは公式Docの例にもあるように二つ以上の階層を指定することも可能です。(Symfony\Core
や Acme\Log\Writer
など)
また一つの名前空間プレフィックスに対して複数のベースディレクトリを登録することも可能です。
loadClassメソッド
最終段階に突入しました。あと一息です。
名前から機能を推測しやすくするためにcallback()
をloadClass()
に変更し、以下のコードを追記してください。
public function loadClass($class)
{
$prefix = $class;
//バクスラ区切りで完全修飾クラス名を分解していき、
//名前空間プレフィックスが$prefixesプロパティに登録されているかを確認していく。
while (false !== $pos = strrpos($prefix, '\\')) {
//プレフィックスと相対クラスに分解
$prefix = substr($class, 0, $pos + 1);
$relative_class = substr($class, $pos + 1);
$mapped_file = $this->loadMappedFile($prefix, $relative_class);
if ($mapped_file) {
return $mapped_file;
}
$prefix = rtrim($prefix, '\\');
}
return false;
}
protected function loadMappedFile($prefix, $relative_class)
{
if (isset($this->prefixes[$prefix]) === false) {
return false;
}
//ベースディレクトリ+相対クラス+拡張子の結合
foreach ($this->prefixes[$prefix] as $base_dir) {
$file = $base_dir
. str_replace('\\', '/', $relative_class)
. '.php';
if ($this->requireFile($file)) {
return $file;
}
}
return false;
}
protected function requireFile($file)
{
//おめでとう!!
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
名前空間がバクスラを使うのに対して、パスがスラッシュを使うので、ほとんどがその処理に追われていますが、やりたいことはいたってシンプルです。
- 完全修飾クラス名を頭からバクスラ区切りで分解しそれぞれ
$prefix
と$relative_class
(相対クラスと呼ぶ)に代入 -
$prefix
が登録されているか確認 - 登録が確認できれば
prefixes[$prefix]
でベースディレクトリを取得
(確認できなければ1. 2.を末尾にたどり着くまで繰り返す) -
$baseDir
,$relative_class
,'.php'
をがっちゃんこ! - ファイルを読み込みッッツツ!!!
(なお各メソッドの返り値でTrue
やFalse
を返している恩恵はテストコードを書く際に得られます。)
動作確認
紐づけ登録を行うmyautoload.php
をindex.php
と同じ階層に作成し下記のコードを記述してください。
<?php
require_once(__DIR__ . "/Autoload.php");
$loader = new Autoload();
$loader->register();
$loader->addNamespace("App", "C:/Users/yanmy/OneDrive/Desktop/Qiita");
最後にindex.php
からmyautoload.php
を読み込んで実行してみましょう。
<?php
require_once "myautoload.php";
use App\Greeting\English as En;
use App\Greeting\Japanese as Ja;
$en = new En\Hello();
$ja = new Ja\Hello();
Autoloadとはお友達になれましたでしょうか?
次回はComposerからAutoloadを使用する方法について解説していきます。
頭が痛い用語解説
-
完全修飾クラス名(Fully qualified class name)
<名前空間+クラス名>
さらに細かく分解すると、
名前空間プレフィックス+サブ名前空間+クラス名
先頭からどこまでを名前空間プレフィックスとするかは任意。
ただし、サブ名前空間とディレクトリ階層は一致しなければならない。 -
名前空間プレフィックス
ベースディレクトリに紐づけする名前空間先頭。(複数階層指定してもよい) -
ベースディレクトリ
名前空間プレフィックスと紐づけされるパス。
読み込みたいファイルパスを生成する際に、起点となるパス。
完全修飾クラス名からは取得できない部分。