1. Home
  2. PHP
  3. PSR
  4. 【PHP】PSR-4 Autoloader(オートローダー)

【PHP】PSR-4 Autoloader(オートローダー)

  • 公開日
  • 更新日
  • カテゴリ:PSR
  • タグ:PHP,PSR,PSR-4,PSR-0
【PHP】PSR-4 Autoloader(オートローダー)

PSR(PHP 標準勧告)

  1. 概要
  2. PSR-1 Basic Coding Standard
  3. PSR-3 Logger Interface
  4. PSR-4 Autoloader
  5. PSR-6 Caching Interface
  6. PSR-7 HTTP Message Interface
  7. PSR-11 Container Interface
  8. PSR-12 Extended Coding Style
  9. PSR-13 Hypermedia Links
  10. PSR-15 HTTP Handlers
  11. PSR-16 Simple Cache
  12. PSR-17 HTTP Factories
  13. PSR-18 HTTP Client

https://www.php-fig.org/psr/psr-4/

PSR-4 は、PHP でのファイルの読み込み・クラスのオートロードを行う仕様について記載されています。

Contents

  1. オートローダ
    1. 仕様
    2. 完全修飾クラス名~ファイルパス例
  2. 実装例
    1. クロージャ
    2. クラス
    3. ユニットテスト
  3. メタドキュメント
    1. 概要
    2. PSR-0 の歴史
    3. Composer
    4. パッケージ指向オートロード
    5. PSR-4 のスコープ
    6. PSR-4 のアプローチ
    7. PSR-0 のみを使用する
    8. PHP 5.3.2 以降の互換性に関する注意

オートローダ

この PSR は、ファイルパスからクラスをオートロードするための仕様を記述しています。 これは完全に相互運用可能で、PSR-0 を含む他の自動ロード仕様に加えて使用することができます。 この PSR は、仕様に従って自動ロードされるファイルの配置場所も記述します。

仕様

「クラス」という用語は、クラス、インタフェース、トレイト、および他の同様の構造を指します。

完全修飾クラス名の形式

完全修飾クラス名の形式は次のとおりです。

\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
  1. 完全修飾クラス名には、トップレベルの名前空間名(「ベンダー名前空間」とも呼ばれます)が必要です。
  2. 完全修飾クラス名は、1 つ以上のサブ名前空間名を持つことができます。
  3. 完全修飾クラス名には、終了クラス名が必要です。
  4. アンダースコアは、完全修飾クラス名のどの部分にも特別な意味を持ちません。
  5. 完全修飾クラス名のアルファベット文字は、小文字と大文字の任意の組み合わせです。
  6. 大文字小文字を区別してすべてのクラス名を参照する必要があります。

ファイルのロード

  1. 完全修飾クラス名(名前空間接頭辞)内の先行する名前空間区切り文字を含まない 1 つ以上の先頭の名前空間およびサブ名前空間名の連続したシリーズは、少なくとも 1 つの「ベースディレクトリ」に対応します。
  2. 「名前空間接頭辞」の後の隣接するサブ名前空間名は、「ベースディレクトリ」内のサブディレクトリに対応し、名前空間区切り記号はディレクトリ区切り記号を表す。 サブディレクトリ名は、サブ名前空間名の大文字と小文字を区別しなければならない。
  3. 終了クラス名は、.php で終わるファイル名に対応します。
  4. ファイル名は終端クラス名の大文字と一致する必要があります。

注意

オートローダーの実装では、例外をスローしたり、レベルのエラーを発生させたりしてはなりません。値を返さないでください。

完全修飾クラス名~ファイルパス例

以下は、指定された完全修飾クラス名、名前空間接頭辞、およびベースディレクトリの対応するファイルパスを示しています。

完全修飾クラス名名前空間接頭辞ベースディレクトリ結果のファイルパス
\Acme\Log\Writer\File_WriterAcme\Log\Writer./acme-log-writer/lib/./acme-log-writer/lib/File_Writer.php
\Aura\Web\Response\StatusAura\Web/path/to/aura-web/src//path/to/aura-web/src/Response/Status.php
\Symfony\Core\RequestSymfony\Core./vendor/Symfony/Core/./vendor/Symfony/Core/Request.php
\Zend\AclZend/usr/includes/Zend//usr/includes/Zend/Acl.php

仕様に準拠したオートローダーの実装例については、実装例 を参照してください。 実装の例は仕様の一部と見なされてはならず、いつでも変更される可能性があります。

実装例

次の例は、PSR-4 に準拠したコードを示しています。

クロージャ

<?php
/**
 * クロージャでの実装例
 *
 * このオートロード機能をSPLに登録した後、次の行は、関数が
 * \Foo\Bar\Baz\Qux クラスを
 * /path/to/project/src/Baz/Qux.phpからロードしようとします:
 *
 *      new \Foo\Bar\Baz\Qux;
 *
 * @param string $class 完全修飾クラス名
 * @return void
 */
spl_autoload_register(function ($class) {

    // プロジェクト固有の名前空間接頭辞
    $prefix = 'Foo\\Bar\\';

    // 名前空間接頭辞のベースディレクトリ
    $base_dir = __DIR__ . '/src/';

    // クラスは名前空間接頭辞を使用しますか?
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        // いいえ、次に登録されたオートローダーに移動します。
        return;
    }

    // 相対クラス名を取得する
    $relative_class = substr($class, $len);

    // 名前空間接頭辞をベースディレクトリに置き換え、
    // 名前空間区切り文字を相対クラス名のディレクトリ区切り文字で置き換え、
    //.phpで追加します
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

    // ファイルが存在する場合は読み込む
    if (file_exists($file)) {
        require $file;
    }
});

クラス

以下は単一の名前空間接頭辞に対して複数のベースディレクトリを許可するオプション機能を含む汎用実装の例です。

次のパスのファイルシステムにクラスの foo-bar パッケージがあるとします。

/path/to/packages/foo-bar/
    src/
        Baz.php             # Foo\Bar\Baz
    Qux/
        Quux.php            # Foo\Bar\Qux\Quux
    tests/
        BazTest.php         # Foo\Bar\BazTest
        Qux/
            QuuxTest.php    # Foo\Bar\Qux\QuuxTest

\Foo\Bar\namespace 接頭辞のクラスファイルへのパスを次のように追加します。

<?php
// ローダをインスタンス化する
$loader = new \Example\Psr4AutoloaderClass;

// オートローダを登録する
$loader->register();

// 名前空間接頭辞のベースディレクトリを登録する
$loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src');
$loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests');

次の行は、オートローダーが /path/to/packages/foo-bar/src/Qux/Quux.php から \Foo\Bar\Qux\Quux クラスをロードしようとします。

<?php
new \Foo\Bar\Qux\Quux;

次の行は、オートローダーが /path/to/packages/foo-bar/tests/Qux/QuuxTest.php から \Foo\Bar\Qux\QuuxTest クラスをロードしようとします。

<?php
new \Foo\Bar\Qux\QuuxTest;

次に、複数の名前空間を処理するためのクラス実装の例を示します。

<?php
namespace Example;

class Psr4AutoloaderClass
{
    /**
     * キーが名前空間プレフィックスであり、値がその名前空間内の
     * クラスの基本ディレクトリの配列である連想配列
     *
     * @var array
     */
    protected $prefixes = array();

    /**
     * ローダーをSPLオートローダスタックに登録する
     *
     * @return void
     */
    public function register()
    {
        spl_autoload_register(array($this, 'loadClass'));
    }

    /**
     * 名前空間接頭辞のベースディレクトリを追加する
     *
     * @param string    $prefix    名前空間接頭辞
     * @param string    $base_dir  名前空間内のクラスファイルのベースディレクトリ
     * @param bool      $prepend   trueの場合はベースディレクトリを追加するのでは
     *                              なくスタックに追加します。 これにより、最後に
     *                              検索されるのではなく最初に検索されます。
     * @return void
     */
    public function addNamespace($prefix, $base_dir, $prepend = false)
    {
        // 名前空間接頭辞を正規化する
        $prefix = trim($prefix, '\\') . '\\';

        // 後続の区切り文字でベースディレクトリを正規化する
        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';

        // 名前空間接頭辞配列を初期化する
        if (isset($this->prefixes[$prefix]) === false) {
            $this->prefixes[$prefix] = array();
        }

        // 名前空間接頭辞のベースディレクトリを保持する
        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $base_dir);
        } else {
            array_push($this->prefixes[$prefix], $base_dir);
        }
    }

    /**
     * 指定されたクラス名のクラスファイルを読み込む
     *
     * @param string $class 完全修飾クラス名
     * @return mixed 成功:マップされたファイル名 失敗:false。
     */
    public function loadClass($class)
    {
        // 現在の名前空間接頭辞
        $prefix = $class;

        // マップされたファイル名を見つけるために、完全修飾クラス名の名前空間名を処理する
        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;
            }

            // strrpos()の次の反復の後続の名前空間区切りを削除する
            $prefix = rtrim($prefix, '\\');
        }

        return false;
    }

    /**
     * 名前空間接頭辞と相対クラスのマッピングされたファイルを読み込む
     *
     * @param   string  $prefix 名前空間接頭辞
     * @param   string  $relative_class 相対クラス名
     * @return  mixed Boolean false ロードされたファイル名(マップされたファイルがロードできない場合はfalse)
     */
    protected function loadMappedFile($prefix, $relative_class)
    {
        // 渡された名前空間プレフィックスのベースディレクトリがなければfalseを返す
        if (isset($this->prefixes[$prefix]) === false) {
            return false;
        }

        // 渡された名前空間接頭辞のベースディレクトリを調べる
        foreach ($this->prefixes[$prefix] as $base_dir) {

            // 名前空間接頭辞をベースディレクトリに置き換え、
            // 名前空間区切り文字を相対クラス名のディレクトリ区切り文字で置き換え、
            // .phpで追加する
            $file = $base_dir
                . str_replace('\\', '/', $relative_class)
                . '.php';

            // マップされたファイルが存在する場合は読み込む
            if ($this->requireFile($file)) {
                return $file;
            }
        }

        return false;
    }

    /**
     * ファイルが存在する場合はファイルシステムから読み込む
     *
     * @param string $file  必要なファイル
     * @return bool True    ファイルが 存在する:false 存在しない:false
     */
    protected function requireFile($file)
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}

ユニットテスト

次の例は、上記のクラスローダの単体テスト方法の 1 つです。

<?php
namespace Example\Tests;

class MockPsr4AutoloaderClass extends Psr4AutoloaderClass
{
    protected $files = array();

    public function setFiles(array $files)
    {
        $this->files = $files;
    }

    protected function requireFile($file)
    {
        return in_array($file, $this->files);
    }
}

class Psr4AutoloaderClassTest extends \PHPUnit_Framework_TestCase
{
    protected $loader;

    protected function setUp()
    {
        $this->loader = new MockPsr4AutoloaderClass;

        $this->loader->setFiles(array(
            '/vendor/foo.bar/src/ClassName.php',
            '/vendor/foo.bar/src/DoomClassName.php',
            '/vendor/foo.bar/tests/ClassNameTest.php',
            '/vendor/foo.bardoom/src/ClassName.php',
            '/vendor/foo.bar.baz.dib/src/ClassName.php',
            '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',
        ));

        $this->loader->addNamespace(
            'Foo\Bar',
            '/vendor/foo.bar/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar',
            '/vendor/foo.bar/tests'
        );

        $this->loader->addNamespace(
            'Foo\BarDoom',
            '/vendor/foo.bardoom/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar\Baz\Dib',
            '/vendor/foo.bar.baz.dib/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar\Baz\Dib\Zim\Gir',
            '/vendor/foo.bar.baz.dib.zim.gir/src'
        );
    }

    public function testExistingFile()
    {
        $actual = $this->loader->loadClass('Foo\Bar\ClassName');
        $expect = '/vendor/foo.bar/src/ClassName.php';
        $this->assertSame($expect, $actual);

        $actual = $this->loader->loadClass('Foo\Bar\ClassNameTest');
        $expect = '/vendor/foo.bar/tests/ClassNameTest.php';
        $this->assertSame($expect, $actual);
    }

    public function testMissingFile()
    {
        $actual = $this->loader->loadClass('No_Vendor\No_Package\NoClass');
        $this->assertFalse($actual);
    }

    public function testDeepFile()
    {
        $actual = $this->loader->loadClass('Foo\Bar\Baz\Dib\Zim\Gir\ClassName');
        $expect = '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php';
        $this->assertSame($expect, $actual);
    }

    public function testConfusion()
    {
        $actual = $this->loader->loadClass('Foo\Bar\DoomClassName');
        $expect = '/vendor/foo.bar/src/DoomClassName.php';
        $this->assertSame($expect, $actual);

        $actual = $this->loader->loadClass('Foo\BarDoom\ClassName');
        $expect = '/vendor/foo.bardoom/src/ClassName.php';
        $this->assertSame($expect, $actual);
    }
}

メタドキュメント

以下はただの和訳+αですが、PSR-0 との関連性や PSR-4 代替への流れなどがわかる部分だけ、読み返す時の為に掲載します。

概要

PSR-4 の目的は、ネームスペースをファイルシステムパスにマップし、他の SPL 登録オートローダーと共存できる相互運用可能な PHP オートローダーのルールを指定することです。 これは PSR-0 に代わるものではありません。

PSR-0 の歴史

PSR-0 クラスの命名と自動ロードの標準は、PHP 5.2 以前の制約の下で、Horde/PEAR規約の幅広い受け入れから誕生しました。 そのような慣例では、すべての PHP ソースクラスを単一のメインディレクトリに置き、クラス名にアンダースコアを使用して疑似名前空間を指定するという傾向がありました。

/path/to/src/
    VendorFoo/
        Bar/
            Baz.php     # VendorFoo_Bar_Baz
    VendorDib/
        Zim/
            Gir.php     # Vendor_Dib_Zim_Gir

PHP 5.3 のリリースとネームスペースの利用可能性により、古い Horde/PEAR アンダースコアモードと新しいネームスペース表記の使用を可能にするために PSR-0 が導入されました。 古いネームスペースのネーミングから新しいネーミングへの移行を容易にするために、クラス名には下線が引き続き使用されていました。

/path/to/src/
    VendorFoo/
        Bar/
            Baz.php     # VendorFoo_Bar_Baz
    VendorDib/
        Zim/
            Gir.php     # VendorDib_Zim_Gir
    Irk_Operation/
        Impending_Doom/
            V1.php
            V2.php      # Irk_Operation\Impending_Doom\V2

この構造は、PEAR インストーラがソースファイルを PEAR パッケージから単一の中央ディレクトリに移動したという事実によって非常によく知られています。

Composer

Composer を使用すると、パッケージソースは単一のグローバルロケーションにコピーされなくなりました。 それらはインストールされた場所から使用され、移動しません。 これは、Composer では PEAR のように PHP ソースのための「単一のメインディレクトリ」が存在しないことを意味します。 代わりに、複数のディレクトリがあります。 各パッケージはプロジェクトごとに別々のディレクトリにあります。

PSR-0 の要件を満たすために、Composer パッケージは次のようになります。

vendor/
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

src と tests ディレクトリにはベンダーとパッケージのディレクトリ名が含まれていなければなりません。 これは PSR-0 準拠の成果物です。

多くの人が、この構造が必要以上に深く、繰り返しがあると感じています。 この提案は、次のようなパッケージを追加することができるように、追加または置き換えられる PSR が有用であることを示唆しています。

vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

これは、当初は「パッケージ指向の自動ロード」と呼ばれていたものを実装する必要がありました(従来の「クラスからファイルへの直接ロード」)。

パッケージ指向オートロード

PSR-0 はクラス名の任意の部分の間に仲介パスを許さないため、PSR-0 の拡張または修正を介してパッケージ指向のオートロードを実装するのは難しいです。 つまり、パッケージ指向のオートローダーの実装は、PSR-0 よりも複雑になります。 しかし、よりクリーンなパッケージングが可能になります。

当初、次のルールが提案されました。

  1. 実装者は、ベンダ名とそのベンダ内のパッケージ名の少なくとも 2 つの名前空間レベルを使用する必要があります。 (この最上位の 2 つの名前の組み合わせを、以下、ベンダーパッケージ名または vendor-packagenamespace と呼びます。)
  2. 実装者は、vendor-package 名前空間と完全修飾されたクラス名の残りの部分との間のパスインフィックスを許可する必要があります。
  3. vendor-package 名前空間は、任意のディレクトリにマップすることができます。 完全修飾クラス名の残りの部分は、名前空間名を同じ名前のディレクトリにマップしなければならず、クラス名を.php で終わる同じ名前のファイルにマップする必要があります。

これはクラス名のアンダースコアとしてのディレクトリ区切り文字の終わりを意味することに注意してください。 psr-0 の下にあるようにアンダースコアは尊重されるべきだと思うかもしれませんが、PHP 5.2 やそれ以前の擬似名前空間からの移行を参照していると見なされます。

PSR-4 のスコープ

  • 実装者が最低 2 つの名前空間レベル(ベンダー名とそのベンダー内のパッケージ名)を使用しなければならないという PSR-0 ルールを保持します。
  • vendor-package 名前空間と完全修飾クラス名の残りの部分の間のパスインフィックスを許可します。
  • ベンダーパッケージの名前空間を任意のディレクトリーに、おそらく複数のディレクトリーにマップできるようにします。
  • クラス名のアンダースコアをディレクトリセパレータとして終了する

PSR-4 のアプローチ

PSR-4 のアプローチは、PSR-0 の重要な特性を保持しながら、必要とするより深いディレクトリ構造を排除します。 さらに、実装で明示的な相互運用性を高めるための追加規則も規定しています。

ディレクトリマッピングには関係しませんが、最終草案では、オートローダがエラーをどのように処理するかについても規定しています。 具体的には、例外のスローやエラーの発生を禁止します。 理由は 2 つです。

  1. PHP のオートローダーは積み重ねられるように明示的に設計されています。そのため、あるオートローダーがクラスをロードできない場合、別のクラスをロードする機会があります。 オートローダのトリガによりブレークエラー状態が発生すると、その互換性に違反します。
  2. class_exists()と interface_exists()は、正規の通常のユースケースとして「自動ロードしようとしても見つからない」ことを許可します。 例外をスローするオートローダーは、class_exists()を使用不可能にします。これは相互運用性の観点からは全く受け入れられません。 クラスが見つからない場合に追加のデバッグ情報を提供したいオートローダーは、代わりに PSR-3 互換のロガーまたはそれ以外の方法でロギングする必要があります。

長所

  • ディレクトリ構造を小さくする
  • より柔軟なファイルの場所
  • クラス名のアンダースコアがディレクトリ区切り文字として認識されないようにする。
  • 実装の明示的な相互運用を可能にする

短所

PSR-0 のように、クラス名を調べてファイルシステム内の場所(Horde/PEAR から継承した class-to-file規則)を調べるだけでは、もはや不可能です。

PSR-0 のみを使用する

合理的ではあるが、PSR-0 だけで済むと、比較的深いディレクトリ構造が残ってしまう。

PHP 5.3.2 以降の互換性に関する注意

5.3.3 より前のバージョンの PHP では、先頭の名前空間の区切り文字を取り除かないので、これを調べる責任は実装に当てはまります。 先頭の名前空間区切り記号を取り除かないと、予期しない動作が発生する可能性があります。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級