PSR 4 自动加载实现范例
下面的范例演示了如何实现 PSR-4
闭包范例
<?php /** * 一个具体项目实现的示例 * * 在注册自动加载函数后,下面这行代码将引发程序 * 尝试从 `/path/to/project/src/Baz/Qux.php` * 加载 `\Foo\Bar\Baz\Qux` 类 * * 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; } });
类范例
下面的代码是一个实现了 PSR-4 且可以处理多命名空间的类
<?php namespace Example; /** * 一个多用途的示例实现,包括了允许多个基本目录使用单个命名空间前缀的可选功能 * * 下述示例创建了一个 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\ 注册到自动加载器中 * * <?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; */ class Psr4AutoloaderClass { /** * 一个关联数组,键名是一个命名空间前缀,值是命名空间的类的基本目录数组 * * @var array */ protected $prefixes = array(); /** * 通过 spl_autoload_register 注册一个自动加载器 * * @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) { // 如果不存在匹配命名空间前缀 ( 组织名 ) 的基础目录? 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 如果文件存在则返回 true,否则返回 false */ protected function requireFile($file) { if (file_exists($file)) { require $file; return true; } return false; } }
单元测试
下面的范例代码是对上面类加载器的单元测试
<?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); } }