PSR-4 自动加载规范 - 示例文档

PSR-4 的实现示例

下面的示例说明了符合 PSR-4 的代码:

闭包示例

  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. });

类示例

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

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

单元测试

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

  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(): void
  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. }