【PSR-4 Autoload】第二回 仕組みを理解したうえでautoloadを実装


## この記事でできるようになること :muscle: 
【第一回】 autoloadの概要を理解
**【第二回】 仕組みを理解したうえでautoloadを実装**
【第三回】 composerからautoloadを利用
## 前回の復習 :thought_balloon:
![Autoload (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2744039/135c664f-56ef-07e3-01ad-6fe62ac67007.png)

## アジェンダ :memo:
- 名前空間がどのような役割を果たすか理解
- PSR-4で使用される用語を理解
- [Class Example](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md/#user-content-class-example)を理解/実装

　今回の最終目標は[**Class Example**](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md/#user-content-class-example)の実装であり、メインテーマは[**名前空間**](https://www.php.net/manual/ja/language.namespaces.php)です。
名前空間を利用し、どんな階層からでもファイルを読み込めるよう`Autoload`クラスを拡張していきます。
## Class Example の解読 :mag:
### 下準備 :gear:
　前回と同じファイルを用いて手を動かしながら理解していきましょう。
今回は別々のディレクトリにある`Hello.php`を`index.php`から読み込むことを目指します。
```:ディレクトリ構成
./Qiita
    ├ App
    │  └ Greeting
    │　　　├ English
    │　　　│  └ Hello.php
    │　　　└ Japanese
    │　　　　 └ Hello.php
    ├ Autoload.php
    └ index.php
```
```App/Greeting/English/Hello.php
<?php

namespace App\Greeting\English;

class Hello
{
    public function __construct()
    {
        echo "Hello World!\n";
    }
}
```
```App/Greeting/Japanese/Hello.php
<?php

namespace App\Greeting\Japanese;

class Hello
{
    public function __construct()
    {
        echo "こんにちは世界。\n";
    }
}

```
まずは、`Autoload`なしで各`Hello`クラスを呼び出してみましょう。
```index.php
<?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();
```
```:Terminal
\Qiita> php index.php
Hello World!
こんにちは世界。
```
問題なく出力できたかと思います。

### 完全修飾クラス名 :thinking: 
次に以下のコードを`index.php`に追記して実行してみてください。
```index.php
var_dump(En\Hello::class);
```

```:terminal
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.

### 【問】ファイルディレクトリを求めよ :hourglass:
#### 【解答】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2744039/d743df4a-ae23-21e8-f0d2-9b3be7d33b54.png)

#### 【解説】
##### 紐づけ:link:
　Autoloadクラスの[最後のチャンス](https://qiita.com/drafts#%E6%9C%80%E5%BE%8C%E3%81%AE%E3%83%81%E3%83%A3%E3%83%B3%E3%82%B9spl_autoload_register-warning)で取得した完全修飾クラス名から、どうにかこうにかしてファイルパスを生成する方法を考えます。
また、**どんな階層からでもファイルを読み込めること**が条件なので、相対パスではなく絶対パスを生成することを考えます。

絶対パスを生成するということは`Qiita`ディレクトリ以前のパスが必要になってきます。
例えば、今回私の環境では`Qiita`ディレクトリは以下のような場所に置かれています。
`C:/Users/yanmy/OneDrive/Desktop/Qiita`

ここで問題なのが、このパスが完全修飾クラス名から取得不可能ということです。
完全修飾クラス名はあくまでも名前空間とクラス名をくっつけた文字列です。

そこで、事前に**紐づけ**を行います。

具体的には名前空間先頭の一部（今回は`App`）を「キー」、先ほどのパスを「値」として保存できる連想配列`$prefixes`プロパティを`Autoload`クラスに作成します。
```Autoload.php
<?php

class Autoload{    
    protected $prefixes = array(); //この行を追記
```
次に紐づけ登録を行うメソッド`addNamespace()`を作成。（一部割愛してます）
```Autoload.php
    //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`プロパティに紐づけした内容が登録されます。
```index.php
$loader = new Autoload();
$loader->addNamespace('App', 'C:/Users/yanmy/OneDrive/Desktop/Qiita/App')
```
ちなみに登録した名前空間の一部を**名前空間プレフィックス**と呼び、
パスを**ベースディレクトリ**と呼びます。別に美味しくはないです。

名前空間プレフィックスは[公式Docの例](https://www.php-fig.org/psr/psr-4/#3-examples)にもあるように二つ以上の階層を指定することも可能です。（`Symfony\Core` や `Acme\Log\Writer`など）
また一つの名前空間プレフィックスに対して複数のベースディレクトリを登録することも可能です。

##### loadClassメソッド :open_file_folder:
最終段階に突入しました。あと一息です。
名前から機能を推測しやすくするために`callback()`を`loadClass()`に変更し、以下のコードを追記してください。

```Autoload.php
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;
}
```

名前空間がバクスラを使うのに対して、パスがスラッシュを使うので、ほとんどがその処理に追われていますが、やりたいことはいたってシンプルです。

1. 完全修飾クラス名を頭からバクスラ区切りで分解しそれぞれ`$prefix`と`$relative_class`（相対クラスと呼ぶ）に代入
2. `$prefix`が登録されているか確認
3. 登録が確認できれば`prefixes[$prefix]`でベースディレクトリを取得
（確認できなければ1. 2.を末尾にたどり着くまで繰り返す）
4. `$baseDir`, `$relative_class`, `'.php'`をがっちゃんこ！
5. ファイルを読み込みッッツツ！！！

（なお各メソッドの返り値で`True`や`False`を返している恩恵はテストコードを書く際に得られます。）

### 動作確認 :wrench:
紐づけ登録を行う`myautoload.php`を`index.php`と同じ階層に作成し下記のコードを記述してください。
```myautoload.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`を読み込んで実行してみましょう。
```index.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を使用する方法について解説していきます。:wave:

## [頭が痛い用語解説](https://www.php-fig.org/psr/psr-4/#3-examples "PHP-FIG公式ドキュメント") :persevere:
- **完全修飾クラス名(Fully qualified class name)**
<名前空間＋クラス名>
さらに細かく分解すると、
**名前空間プレフィックス**+サブ名前空間+クラス名
先頭からどこまでを名前空間プレフィックスとするかは任意。
ただし、サブ名前空間とディレクトリ階層は一致しなければならない。


- **名前空間プレフィックス**
ベースディレクトリに紐づけする名前空間先頭。（複数階層指定してもよい）

- **ベースディレクトリ**
名前空間プレフィックスと紐づけされるパス。
読み込みたいファイルパスを生成する際に、起点となるパス。
完全修飾クラス名からは取得できない部分。