PSR-4 自动加载规范

一、说明

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

必须 (MUST):绝对,严格遵循,请照做,无条件遵守;

一定不可 (MUST NOT):禁令,严令禁止;

应该 (SHOULD) :强烈建议这样做,但是不强求;

不该 (SHOULD NOT):强烈不建议这样做,但是不强求;

 

二、总览

PSR-4 描述了从文件路径中 自动加载 类的规范。

它拥有非常好的兼容性,并且可以在任何自动加载规范中使用,包括 PSR-0。 PSR-4 规范也描述了放置 autoload 文件(就是我们经常引入的 vendor/autoload.php)的位置。

 

三、示例

 
1、闭包示例

  1. <?php
  2. /**
  3. * 一个具体项目实现的示例。
  4. *
  5. * 在注册自动加载函数后,下面这行代码将引发程序
  6. * 尝试从 /path/to/project/src/Baz/Qux.php
  7. * 加载 \Foo\Bar\Baz\Qux 类:
  8. *
  9. * new \Foo\Bar\Baz\Qux;
  10. *
  11. * @param string $class 完全标准的类名。
  12. * @return void
  13. */
  14. spl_autoload_register(function ($class) {
  15. // 具体项目的命名空间前缀
  16. $prefix = 'Foo\\Bar\\';
  17. // 命名空间前缀对应的基础目录
  18. $base_dir = __DIR__ . '/src/';
  19. // 该类使用了此命名空间前缀?
  20. $len = strlen($prefix);
  21. if (strncmp($prefix, $class, $len) !== 0) {
  22. // 否,交给下一个已注册的自动加载函数
  23. return;
  24. }
  25. // 获取相对类名
  26. $relative_class = substr($class, $len);
  27. // 命名空间前缀替换为基础目录,
  28. // 将相对类名中命名空间分隔符替换为目录分隔符,
  29. // 附加 .php
  30. $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
  31. // 如果文件存在,加载它
  32. if (file_exists($file)) {
  33. require $file;
  34. }
  35. });

 
2、类示例

以下是一个可处理多命名空间的类实现示例:

  1. <?php
  2. namespace Example;
  3. /**
  4. * 一个多用途的示例实现,包括了
  5. * 允许多个基本目录用于单个
  6. * 命名空间前缀的可选功能
  7. */
  8. class Psr4AutoloaderClass
  9. {
  10. /**
  11. * 关联数组,键名为命名空间前缀,键值为一个基本目录数组。
  12. *
  13. * @var array
  14. */
  15. protected $prefixes = array();
  16. /**
  17. * 通过 SPL 自动加载器栈注册加载器
  18. *
  19. * @return void
  20. */
  21. public function register()
  22. {
  23. spl_autoload_register(array($this, 'loadClass'));
  24. }
  25. /**
  26. * 为命名空间前缀添加一个基本目录
  27. *
  28. * @param string $prefix 命名空间前缀。
  29. * @param string $base_dir 命名空间下类文件的基本目录
  30. * @param bool $prepend 如果为真,预先将基本目录入栈
  31. * 而不是后续追加;这将使得它会被首先搜索到。
  32. * @return void
  33. */
  34. public function addNamespace($prefix, $base_dir, $prepend = false)
  35. {
  36. // 规范化命名空间前缀
  37. $prefix = trim($prefix, '\\') . '\\';
  38. // 规范化尾部文件分隔符
  39. $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
  40. // 初始化命名空间前缀数组
  41. if (isset($this->prefixes[$prefix]) === false) {
  42. $this->prefixes[$prefix] = array();
  43. }
  44. // 保留命名空间前缀的基本目录
  45. if ($prepend) {
  46. array_unshift($this->prefixes[$prefix], $base_dir);
  47. } else {
  48. array_push($this->prefixes[$prefix], $base_dir);
  49. }
  50. }
  51. /**
  52. * 加载给定类名的类文件
  53. *
  54. * @param string $class 合法类名
  55. * @return mixed 成功时为已映射文件名,失败则为 false
  56. */
  57. public function loadClass($class)
  58. {
  59. // 当前命名空间前缀
  60. $prefix = $class;
  61. // 通过完整的命名空间类名反向映射文件名
  62. while (false !== $pos = strrpos($prefix, '\\')) {
  63. // 在前缀中保留命名空间分隔符
  64. $prefix = substr($class, 0, $pos + 1);
  65. // 其余的是相关类名
  66. $relative_class = substr($class, $pos + 1);
  67. // 尝试为前缀和相关类加载映射文件
  68. $mapped_file = $this->loadMappedFile($prefix, $relative_class);
  69. if ($mapped_file) {
  70. return $mapped_file;
  71. }
  72. // 删除 strrpos() 下一次迭代的尾部命名空间分隔符
  73. $prefix = rtrim($prefix, '\\');
  74. }
  75. // 找不到映射文件
  76. return false;
  77. }
  78. /**
  79. * 为命名空间前缀和相关类加载映射文件。
  80. *
  81. * @param string $prefix 命名空间前缀
  82. * @param string $relative_class 相关类
  83. * @return mixed Boolean 无映射文件则为false,否则加载映射文件
  84. */
  85. protected function loadMappedFile($prefix, $relative_class)
  86. {
  87. // 命名空间前缀是否存在任何基本目录
  88. if (isset($this->prefixes[$prefix]) === false) {
  89. return false;
  90. }
  91. // 通过基本目录查找命名空间前缀
  92. foreach ($this->prefixes[$prefix] as $base_dir) {
  93. // 用基本目录替换命名空间前缀
  94. // 用目录分隔符替换命名空间分隔符
  95. // 给相关的类名增加 .php 后缀
  96. $file = $base_dir
  97. . str_replace('\\', '/', $relative_class)
  98. . '.php';
  99. // 如果映射文件存在,则引入
  100. if ($this->requireFile($file)) {
  101. // 搞定了
  102. return $file;
  103. }
  104. }
  105. // 找不到
  106. return false;
  107. }
  108. /**
  109. * 如果文件存在从系统中引入进来
  110. *
  111. * @param string $file 引入文件
  112. * @return bool 文件存在则 true 否则 false
  113. */
  114. protected function requireFile($file)
  115. {
  116. if (file_exists($file)) {
  117. require $file;
  118. return true;
  119. }
  120. return false;
  121. }
  122. }

 
3、单元测试

以下示例是上述类加载器的单元测试方式之一:

  1. <?php
  2. namespace Example\Tests;
  3. class MockPsr4AutoloaderClass extends Psr4AutoloaderClass
  4. {
  5. protected $files = array();
  6. public function setFiles(array $files)
  7. {
  8. $this->files = $files;
  9. }
  10. protected function requireFile($file)
  11. {
  12. return in_array($file, $this->files);
  13. }
  14. }
  15. class Psr4AutoloaderClassTest extends \PHPUnit_Framework_TestCase
  16. {
  17. protected $loader;
  18. protected function setUp()
  19. {
  20. $this->loader = new MockPsr4AutoloaderClass;
  21. $this->loader->setFiles(array(
  22. '/vendor/foo.bar/src/ClassName.php',
  23. '/vendor/foo.bar/src/DoomClassName.php',
  24. '/vendor/foo.bar/tests/ClassNameTest.php',
  25. '/vendor/foo.bardoom/src/ClassName.php',
  26. '/vendor/foo.bar.baz.dib/src/ClassName.php',
  27. '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',
  28. ));
  29. $this->loader->addNamespace(
  30. 'Foo\Bar',
  31. '/vendor/foo.bar/src'
  32. );
  33. $this->loader->addNamespace(
  34. 'Foo\Bar',
  35. '/vendor/foo.bar/tests'
  36. );
  37. $this->loader->addNamespace(
  38. 'Foo\BarDoom',
  39. '/vendor/foo.bardoom/src'
  40. );
  41. $this->loader->addNamespace(
  42. 'Foo\Bar\Baz\Dib',
  43. '/vendor/foo.bar.baz.dib/src'
  44. );
  45. $this->loader->addNamespace(
  46. 'Foo\Bar\Baz\Dib\Zim\Gir',
  47. '/vendor/foo.bar.baz.dib.zim.gir/src'
  48. );
  49. }
  50. public function testExistingFile()
  51. {
  52. $actual = $this->loader->loadClass('Foo\Bar\ClassName');
  53. $expect = '/vendor/foo.bar/src/ClassName.php';
  54. $this->assertSame($expect, $actual);
  55. $actual = $this->loader->loadClass('Foo\Bar\ClassNameTest');
  56. $expect = '/vendor/foo.bar/tests/ClassNameTest.php';
  57. $this->assertSame($expect, $actual);
  58. }
  59. public function testMissingFile()
  60. {
  61. $actual = $this->loader->loadClass('No_Vendor\No_Package\NoClass');
  62. $this->assertFalse($actual);
  63. }
  64. public function testDeepFile()
  65. {
  66. $actual = $this->loader->loadClass('Foo\Bar\Baz\Dib\Zim\Gir\ClassName');
  67. $expect = '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php';
  68. $this->assertSame($expect, $actual);
  69. }
  70. public function testConfusion()
  71. {
  72. $actual = $this->loader->loadClass('Foo\Bar\DoomClassName');
  73. $expect = '/vendor/foo.bar/src/DoomClassName.php';
  74. $this->assertSame($expect, $actual);
  75. $actual = $this->loader->loadClass('Foo\BarDoom\ClassName');
  76. $expect = '/vendor/foo.bardoom/src/ClassName.php';
  77. $this->assertSame($expect, $actual);
  78. }
  79. }

 

 



Top