LoginSignup
1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-13

この記事でできるようになること :muscle:

【第一回】 autoloadの概要を理解
【第二回】 仕組みを理解したうえでautoloadを実装
【第三回】 composerからautoloadを利用

前回の復習 :thought_balloon:

Autoload (1).png

アジェンダ :pencil:

  • 名前空間がどのような役割を果たすか理解
  • PSR-4で使用される用語を理解
  • Class Exampleを理解/実装

 今回の最終目標はClass Exampleの実装であり、メインテーマは名前空間です。
名前空間を利用し、どんな階層からでもファイルを読み込めるようAutoloadクラスを拡張していきます。

Class Example の解読 :mag:

下準備 :gear:

 前回と同じファイルを用いて手を動かしながら理解していきましょう。
今回は別々のディレクトリにあるHello.phpindex.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

【解説】

紐づけ:link:

 Autoloadクラスの最後のチャンスで取得した完全修飾クラス名から、どうにかこうにかしてファイルパスを生成する方法を考えます。
また、どんな階層からでもファイルを読み込めることが条件なので、相対パスではなく絶対パスを生成することを考えます。

絶対パスを生成するということは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の例にもあるように二つ以上の階層を指定することも可能です。(Symfony\CoreAcme\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. ファイルを読み込みッッツツ!!!

(なお各メソッドの返り値でTrueFalseを返している恩恵はテストコードを書く際に得られます。)

動作確認 :wrench:

紐づけ登録を行うmyautoload.phpindex.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:

頭が痛い用語解説 :persevere:

  • 完全修飾クラス名(Fully qualified class name)
    <名前空間+クラス名>
    さらに細かく分解すると、
    名前空間プレフィックス+サブ名前空間+クラス名
    先頭からどこまでを名前空間プレフィックスとするかは任意。
    ただし、サブ名前空間とディレクトリ階層は一致しなければならない。

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0