PSR-4 PHP自动加载规范解读

不得不提到的 PSR-0规范

PSR-0是什么鬼?

在PHP 5.2及之前的限制下,PSR-0类命名和自动加载标准在Horde / PEAR公约的广泛接受之后被大量使用。 根据该约定,趋势是将所有PHP源类放在单个主目录中,使用类名中的下划线来指示伪命名空间,如下所示:

/path/to/src/
    VendorFoo/
        Bar/
            Baz.php     # VendorFoo_Bar_Baz
    VendorDib/
        Zim/
            Gir.php     # Vendor_Dib_Zim_Gir
1
2
3
4
5
6
7

随着PHP 5.3的发布和命名空间的引入,PSR-0不得不同时支持旧的 “Horde / PEAR下划线模式” 和新的命名空间形式。 类名中仍然允许使用下划线,以简化从较旧的命名空间到较新命名空间机制的过渡。

/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
1
2
3
4
5
6
7
8
9
10
11

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
1
2
3
4
5
6
7
8
9
10
11

“src”和“tests”这2个目录必须包含完全相同供应商和包目录名称,才能符合PSR-0标准。

许多人发现这种结构根本没有必要,因为它导致非常深的目录结构,而且非常重复。 并因此提议添加或替代PSR-0,以便我们可以使用更像以下结构的软件包:

vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest
1
2
3
4
5
6
7

这将需要去实现一个被称为以包为导向的自动加载机制,从而取代传统的面向类的自动加载机制.

以包为导向的自动加载

通过对PSR-0的扩展或修改,来实现以包为导向的自动加载很困难,因为PSR-0不允许类名的任何部分之间的代理路径。 这意味着以包为导向的自动加载的自动加载器的实现将比PSR-0更复杂。 但是,它将允许软件包更加简洁。

最初,建议遵循以下规则:

  1. 以包为导向的自动加载器必须至少使用两个命名空间级别:供应商名称和该供应商中的包名称。 (此顶级双名组合在下文中称为供应商包名称或供应商包名称空间。)
  2. 以包为导向的自动加载器必须允许vendor-package命名空间和完全限定类名的其余部分之间的路径中缀。
  3. vendor-package命名空间可以映射到任何目录。 完全限定类名的剩余部分必须将命名空间名称映射到具有相同名称的目录,并且必须将类名映射到以.php结尾的同名文件。

请注意,这意味着不能在类名中使用下划线作为目录分隔符了。 有人可能认为下划线应该受到尊重,因为它们属于PSR-0,但是看到它们在该文档中的存在是因为要从PHP 5.2和之前的伪命名空间时代过渡,因此大家也都接受了这个新的机制


PSR-4 规范

要达成的目的

  • 保留PSR-0规则,实现者必须至少使用两个命名空间级别:供应商名称和该供应商中的包名称。
  • 允许vendor-package命名空间与完全限定类名的其余部分之间的路径中缀。
  • 允许vendor-package命名空间MAY映射到任何目录,可能是多个目录。
  • 不再将类名称中的下划线作为目录分隔符

不包含的内容

  • 为非php类文件的资源提供通用转换算法

PSR-4规范的出炉过程

最终方案的思考分析

最终方案: 保留了PSR-0的关键特性,同时消除了它所需的更深层的目录结构。 此外,它还指定了某些其他规则,这些规则使实现更具互操作性。

尽管与目录映射无关,但最终草案还指定了自动加载器应如何处理错误。 具体来说,它禁止抛出异常或引发错误。 原因有以下2个:

  1. PHP中的自动加载器明确设计为可堆叠,因此如果一个自动加载器无法加载类,则另一个自动加载器有机会这样做。 让自动装带器触发断开错误条件会违反该兼容性。
  2. class_exists()和interface_exists()允许“找不到,即使在尝试自动加载后”作为合法的正常情况。 抛出异常的自动加载器会使class_exists()无法使用,从互操作性的角度来看,这是完全不可接受的。 希望在类未找到的情况下提供额外调试信息的自动加载器应该通过日志记录来执行此操作,或者使用PSR-3兼容的记录器或其他方式。

优点:

  • 浅层目录结构
  • 更灵活的文件位置
  • 阻止类名中的下划线被视为目录分隔符
  • 使实现更加明确地可互操作

缺点:

  • 在PSR-0下,不再可能只检查一个类名来确定它在文件系统中的位置(从Horde / PEAR继承的“类到文件”约定)

备选方案2的思考分析

继续使用PSR-0, 但确实相对较深的目录结构很烦人

优点:

  • 无需改变

缺点:

  • 让我们有更深入的目录结构
  • 给我们留下类名中的下划线作为目录分隔符

备选方案3的思考分析

拆分自动加载和转换

Beau Simensen和其他人建议转换算法可能会从自动加载提案中分离出来,以便转换规则可以被其他提案引用。 在完成分离它们的工作之后,再进行轮询和讨论,组合版本(即嵌入在自动装带器提议中的转换规则)被视为首选项。

优点:

  • 转换规则可以由其他提案单独引用

缺点:

  • 不符合民意调查受访者和一些合作者的意愿

备选方案4的思考分析

使用更多命令式和叙事性语言

在多个+1选民听到他们支持这个想法但未同意(或理解)提案的措辞后,赞助商撤回了第二次投票后,有一段时间,投票通过的提案得到了扩展。 更大的叙事和更有必要的语言。 少数参与者谴责这种方法。 过了一段时间,Beau Simensen开始进行实验性修订,着眼于PSR-0; 编辑和赞助商赞成采用这种更简洁的方法,并指导现在正在考虑的版本,由Paul M. Jones编写并为许多人做出贡献。

与PHP 5.3.2及更低版本的兼容性说明 5.3.3之前的PHP版本不会删除前导命名空间分隔符,因此需要注意的是实现。 无法删除前导命名空间分隔符可能会导致意外行为。

最终实现的样例代码

类加载器

<?php
/**
 * 在向 SPL 注册了这个加载器方法之后, 语句 
 *      new \Foo\Bar\Baz\Qux; 
 * 将试图加载 
 *      /path/to/project/src/Baz/Qux.php 文件
 *
 * @param string $class 全路径的类名,如 '\Foo\Bar\Baz\Qux'
 * @return void
 */
spl_autoload_register(function ($class) {

    // 库命名空间前缀
    $prefix = 'Foo\\Bar\\';

    // 库命名空间前缀对应的目录总是 '库的根目录/src/'
    $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';

    // 如果上面构造出来的php文件存在,就require它
    if (file_exists($file)) {
        require $file;
    }

    // 注意:是不需要return的
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39