RitoLabo

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

  • 公開:
  • 更新:
  • カテゴリ: PHP PSR
  • タグ: PHP,PSR,PSR-4,PSR-0

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

目次
  1. オートローダ
    1. 仕様
      1. 完全修飾クラス名の形式
      2. ファイルのロード
      3. 注意
    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_Writer Acme\Log\Writer ./acme-log-writer/lib/ ./acme-log-writer/lib/File_Writer.php
\Aura\Web\Response\Status Aura\Web /path/to/aura-web/src/ /path/to/aura-web/src/Response/Status.php
\Symfony\Core\Request Symfony\Core ./vendor/Symfony/Core/ ./vendor/Symfony/Core/Request.php
\Zend\Acl Zend /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

srctestsディレクトリにはベンダーとパッケージのディレクトリ名が含まれていなければなりません。 これは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では、先頭の名前空間の区切り文字を取り除かないので、これを調べる責任は実装に当てはまります。 先頭の名前空間区切り記号を取り除かないと、予期しない動作が発生する可能性があります。