FIG-PHP PSR规范系列4-自动加载

1.  PSR-4规范:自动加载

虽然在[PSR-4-Meta]中指出PSR-4是对PSR-0规范的补充而不是替换,但是在[PSR-0]中已经写到PSR-0于2014.10.21被废弃,并在[PSR-4-Meta]中详细写明了PSR-0的不足,已经不能满足面向package的自动加载。

PSR-4规范能够满足面向package的自动加载,它规范了如何从文件路径自动加载类,同时规范了自动加载文件的位置。

1.1 概述

这份PSR规范描述了从文件路径自动加载类。可以与PSR-0规范互操作,可以一起使用。这份PSR也描述了自动加载的文件应当放在哪里。

1.2 规范

1.2.1 术语"class"是指classes, interfaces, traits, 以及其他类似的结构.

1.2.2 一个完全合乎规格的类名(A fully qualified class name)格式如下:

\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>

(1) 完全合规的类名必须(MUST)有一个顶级命名空间名称,也就是通常所说的"vendor命名空间".

(2) 完全合规的类名可以(MAY)有一个或多个二级命名空间名称(sub-namespace names).

(3) 完全合规的类名必须(MUST)以类名来结尾。

(4) 在完全合规的类名的任意一个部分,下划线都没有特殊的含义。

(5) 在完全合规的类名中,可以(MAY)是任意大小写字母混合。

(6) 所有的类名必须(MUST)按大小写敏感方式来引用。

1.2.3 当加载完全合规的类名对应的文件时...

(1) 在完全合规的类名中, 不包含前面的命名空间分隔符,由一个顶级命名空间与一个或多个二级命名空间名称组成的命名空间前缀,对应于至少一个“base目录”.

(2) 在命名空间前缀后面的二级命名空间名称对应于“base目录”中的一个子目录, 这里命名空间分隔符表示目录分隔符。子目录名称必须(MUST)匹配到二级命名空间名称。

(3) 后面的类名对应于以.php为后缀的文件名,这个文件名必须(MUST)匹配到后面的类名。

(4) 自动加载实现一定不能(MUST NOT)抛出异常,一定不能(MUST NOT)引发任何级别的错误, 并且不应当(SHOULD NOT)返回值。

1.3. 举例

下面的表展示了对一个完全合规的类名, 命名空间前缀以及base目录对应的文件路径.

完全合规类名 命名空间前缀 base目录 最终的文件路径
\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

备注:以第一行为例来说明,完全合规的类名是“\Acme\Log\Writer\File_Writer”, 去掉前面的命名空间分隔符‘\‘, 则命名空间前缀为"Acme\Log\Writer", 类名为"File_Writer"。这个命名空间前缀对应的base目录为"./acme-log-writer/lib/", 因此最终加载的文件名为:base目录+类名+".php", 即"./acme-log-writer/lib/File_Writer.php"

遵循本规范的自动加载器的实现举例, 可参见下面的代码样例。这些实现样例一定不能(MUST NOT)被视为本规范的内容,它们可能(MAY)随时发生改变。

2. 代码样例

以下代码展示了遵循PSR-4的类定义,

闭包(Closure)举例:

<?php
/**
 * An example of a project-specific implementation.
 * 
 * After registering this autoload function with SPL, the following line
 * would cause the function to attempt to load the \Foo\Bar\Baz\Qux class
 * from /path/to/project/src/Baz/Qux.php:
 * 
 *      new \Foo\Bar\Baz\Qux;
 *      
 * @param string $class The fully-qualified class name.
 * @return void
 */
spl_autoload_register(function ($class) {

    // project-specific namespace prefix
    // 项目的命名空间前缀
    $prefix = ‘Foo\\Bar\\‘;

    // base directory for the namespace prefix
    // 命名空间前缀对应的base目录
    $base_dir = __DIR__ . ‘/src/‘;

    // does the class use the namespace prefix?
    // 检查$class中是否包含命名空间前缀
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        // no, move to the next registered autoloader
        // 未包含,立即返回
        return;
    }

    // get the relative class name
    // 获取相对类名
    $relative_class = substr($class, $len);

    // replace the namespace prefix with the base directory, replace namespace
    // separators with directory separators in the relative class name, append
    // with .php
    // 用base目录替代命名空间前缀, 
    // 在相对类名中用目录分隔符‘/‘来替换命名空间分隔符‘\‘, 
    // 并在后面追加.php组成$file的绝对路径
    $file = $base_dir . str_replace(‘\\‘, ‘/‘, $relative_class) . ‘.php‘;

    // if the file exists, require it
    // 如果文件存在,则通过require关键字包含文件
    if (file_exists($file)) {
        require $file;
    }
});

下面这个类处理多个命名空间:

<?php
namespace Example;

/**
 * An example of a general-purpose implementation that includes the optional
 * functionality of allowing multiple base directories for a single namespace
 * prefix.
 * 下面例子中在一个命名空间前缀下有多个base目录。
 * 
 * Given a foo-bar package of classes in the file system at the following
 * paths ...
 * 在下面路径中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
 * 
 * ... add the path to the class files for the \Foo\Bar\ namespace prefix
 * as follows:
 * ...对\Foo\Bar\命名空间前缀,添加类文件的路径
 * 
 *      <?php
 *      // instantiate the loader
 *      // 初始化loader 
 *      $loader = new \Example\Psr4AutoloaderClass;
 *      
 *      // register the autoloader
 *      // 注册autoloader
 *      $loader->register();
 *      
 *      // register the base directories for the namespace prefix
 *      // 注册命名空间前缀的多个base目录
 *      $loader->addNamespace(‘Foo\Bar‘, ‘/path/to/packages/foo-bar/src‘);
 *      $loader->addNamespace(‘Foo\Bar‘, ‘/path/to/packages/foo-bar/tests‘);
 * 
 * The following line would cause the autoloader to attempt to load the
 * \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php:
 * 下面代码将用/path/to/packages/foo-bar/src/Qux/Quux.php文件来加载\Foo\Bar\Qux\Quux类。
 * 
 *      <?php
 *      new \Foo\Bar\Qux\Quux;
 * 
 * The following line would cause the autoloader to attempt to load the 
 * \Foo\Bar\Qux\QuuxTest class from /path/to/packages/foo-bar/tests/Qux/QuuxTest.php:
 * 下面代码将用/path/to/packages/foo-bar/tests/Qux/QuuxTest.php文件来加载
 * \Foo\Bar\Qux\QuuxTest类。
 * 
 *      <?php
 *      new \Foo\Bar\Qux\QuuxTest;
 */
class Psr4AutoloaderClass
{
    /**
     * An associative array where the key is a namespace prefix and the value
     * is an array of base directories for classes in that namespace.
     * 定义一个数组:key为命名空间前缀,value为一个数组,每一项表示命名空间中类对应的base目录.
     *
     * @var array
     */
    protected $prefixes = array();

    /**
     * Register loader with SPL autoloader stack.
     * 利用SPL自动加载器来注册loader
     * 
     * @return void
     */
    public function register()
    {
        spl_autoload_register(array($this, ‘loadClass‘));
    }

    /**
     * Adds a base directory for a namespace prefix.
     * 为一个命名空间前缀添加对应的base目录
     *
     * @param string $prefix The namespace prefix.
     * @param string $base_dir A base directory for class files in the
     * namespace.
     * @param bool $prepend If true, prepend the base directory to the stack
     * instead of appending it; this causes it to be searched first rather
     * than last.
     * @return void
     */
    public function addNamespace($prefix, $base_dir, $prepend = false)
    {
        // normalize namespace prefix
        // 规范命名空间前缀
        $prefix = trim($prefix, ‘\\‘) . ‘\\‘;

        // normalize the base directory with a trailing separator
        // 用‘/‘字符来规范base目录
        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . ‘/‘;

        // initialize the namespace prefix array
        // 初始化命名空间前缀数组
        if (isset($this->prefixes[$prefix]) === false) {
            $this->prefixes[$prefix] = array();
        }

        // retain the base directory for the namespace prefix
        // 绑定命名空间前缀对应的base目录
        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $base_dir);
        } else {
            array_push($this->prefixes[$prefix], $base_dir);
        }
    }

    /**
     * Loads the class file for a given class name.
     * 根据类名来加载类文件。
     *
     * @param string $class The fully-qualified class name.
     * @return mixed The mapped file name on success, or boolean false on
     * failure.
     */
    public function loadClass($class)
    {
        // the current namespace prefix
        $prefix = $class;

        // work backwards through the namespace names of the fully-qualified
        // class name to find a mapped file name
        // 从后面开始遍历完全合格类名中的命名空间名称, 来查找映射的文件名
        while (false !== $pos = strrpos($prefix, ‘\\‘)) {

            // retain the trailing namespace separator in the prefix
            // 保留命名空间前缀中尾部的分隔符
            $prefix = substr($class, 0, $pos + 1);

            // the rest is the relative class name
            // 剩余的就是相对类名称
            $relative_class = substr($class, $pos + 1);

            // try to load a mapped file for the prefix and relative class
            // 利用命名空间前缀和相对类名来加载映射文件
            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
            if ($mapped_file) {
                return $mapped_file;
            }

            // remove the trailing namespace separator for the next iteration
            // of strrpos()
            // 删除命名空间前缀尾部的分隔符,以便用于下一次strrpos()迭代
            $prefix = rtrim($prefix, ‘\\‘);   
        }

        // never found a mapped file
        // 未找到映射文件
        return false;
    }

    /**
     * Load the mapped file for a namespace prefix and relative class.
     * 根据命名空间前缀和相对类来加载映射文件
     * 
     * @param string $prefix The namespace prefix.
     * @param string $relative_class The relative class name.
     * @return mixed Boolean false if no mapped file can be loaded, or the
     * name of the mapped file that was loaded.
     */
    protected function loadMappedFile($prefix, $relative_class)
    {
        // are there any base directories for this namespace prefix?
        // 命名空间前缀中有base目录吗?
        if (isset($this->prefixes[$prefix]) === false) {
            return false;
        }

        // look through base directories for this namespace prefix
        // 遍历命名空间前缀的base目录
        foreach ($this->prefixes[$prefix] as $base_dir) {

            // replace the namespace prefix with the base directory,
            // replace namespace separators with directory separators
            // in the relative class name, append with .php
            // 用base目录替代命名空间前缀, 
            // 在相对类名中用目录分隔符‘/‘来替换命名空间分隔符‘\‘, 
            // 并在后面追加.php组成$file的绝对路径
            $file = $base_dir
                  . str_replace(‘\\‘, ‘/‘, $relative_class)
                  . ‘.php‘;

            // if the mapped file exists, require it
            // 若映射文件存在,则require该文件
            if ($this->requireFile($file)) {
                // yes, we‘re done
                return $file;
            }
        }

        // never found it
        return false;
    }

    /**
     * If a file exists, require it from the file system.
     * 
     * @param string $file The file to require.
     * @return bool True if the file exists, false if not.
     */
    protected function requireFile($file)
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}

3. 单元测试

下面是对应的单元测试代码:

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

4. PSR-4应用

PHP的包管理系统Composer已经支持PSR-4,同时也允许在composer.json中定义不同的prefix使用不同的自动加载机制。

Composer使用PSR-0风格

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\ClassName

Composer使用PSR-4风格

vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

对比以上两种结构,明显可以看出PSR-4带来更简洁的文件结构。

5. 参考资料

[PHP-FIG] php-fig, http://www.php-fig.org/

[PSR-0] Autoloading Standard, http://www.php-fig.org/psr/psr-0/

[PSR-4] Autoloader, http://www.php-fig.org/psr/psr-4/

[PSR-4-Meta] PSR-4 Meta Document, http://www.php-fig.org/psr/psr-4/meta/

[PSR-4-Example] Example Implementations of PSR-4, http://www.php-fig.org/psr/psr-4/examples/

时间: 2024-11-13 08:04:04

FIG-PHP PSR规范系列4-自动加载的相关文章

FIG-PHP PSR规范系列1-基础编码规范

1. FIG-PHP与PSR简介 FIG-PHP制定了一系列PHP开发规范,简称PSR,这里FIG是框架互操作工作组(Framework Interoperability Group)的简称,PSR是PHP标准推荐(PHP Standard Recommendation)的缩写.FIG-PHP工作组最初是源于项目代表讨论两个项目之间的共性时,找出可以共事的方式.主要的受众是双方项目组,但PHP界的其他人也在观望.如果其他人愿意采用这里的规范,那么欢迎,但这并非工作组的目标.工作组中没人会告诉你如

深入解析 composer 的自动加载原理 (转)

深入解析 composer 的自动加载原理 转自:https://segmentfault.com/a/1190000014948542 前言 PHP 自5.3的版本之后,已经重焕新生,命名空间.性状(trait).闭包.接口.PSR 规范.以及 composer 的出现已经让 PHP 变成了一门现代化的脚本语言.PHP 的生态系统也一直在演进,而 composer 的出现更是彻底的改变了以往构建 PHP 应用的方式,我们可以根据 PHP 的应用需求混合搭配最合适的 PHP 组件.当然这也得益于

PSR 类自动加载规范的翻译与看法

先列举一些资源: PSR-0官网原文 PSR的Github源 PSR中文翻译的Github源 有几点事先说明: 翻译部分,我只挑选PSR-0和PSR-4中的主要规范内容进行翻译. 我的翻译,侧重以理解的角度,而不是严格的文法翻译. 关键修饰词的说明: **MUST** __务必__ **MUST_NOT** __绝不__ **REQUIRED** __务必__ **SHALL** __务必__ **SHALL_NOT** __绝不__ **SHOULD** __应该__ **SHOULD_NOT

如何实现一个php框架系列文章【3】支持psr4的自动加载类

psr4自动加载规范https://github.com/PizzaLiu/PHP-FIG/blob/master/PSR-4-autoloader-cn.md 我们把第三方使用psr规范的类库放在vendor目录下 修改一下autoload函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22     //psr     if (!empty($GLOBALS['_UCT']['autoload_psr'])) {        

PSR-4 自动加载规范

关键词 "必须"("MUST")."一定不可/一定不能"("MUST NOT")."需要"("REQUIRED"). "将会"("SHALL")."不会"("SHALL NOT")."应该"("SHOULD")."不该"("SHOULD N

thinkphp5源码剖析系列1-类的自动加载机制

前言 tp5想必大家都不陌生,但是大部分人都停留在应用的层面,我将开启系列随笔,深入剖析tp5源码,以供大家顺利进阶.本章将从类的自动加载讲起,自动加载是tp框架的灵魂所在,也是成熟php框架的必备功能 入口 // [ 应用入口文件 ] namespace think; // 加载基础文件 require __DIR__ . '/../thinkphp/base.php'; base.php <?php // +------------------------------------------

laravel的源码解析:PHP自动加载功能原理解析

前言 这篇文章是对PHP自动加载功能的一个总结,内容涉及PHP的自动加载功能.PHP的命名空间.PHP的PSR0与PSR4标准等内容. 一.PHP自动加载功能 PHP自动加载功能的由来 在PHP开发过程中,如果希望从外部引入一个 class,通常会使用 include 和 require 方法,去把定义这个 class 的文件包含进来.这个在小规模开发的时候,没什么大问题.但在大型的开发项目中,使用这种方式会带来一些隐含的问题:如果一个 PHP 文件需要使用很多其它类,那么就需要很多的 requ

Yii2的深入学习--自动加载机制

Yii2 的自动加载分两部分,一部分是 Composer 的自动加载机制,另一部分是 Yii2 框架自身的自动加载机制. Composer自动加载 对于库的自动加载信息,Composer 生成了一个 vendor/autoload.php 文件.你可以简单的引入这个文件,你会得到一个自动加载的支持. 在之前的文章,入口文件的介绍中,我们可以看到如下内容: // 引入 vendor 中的 autoload.php 文件,会基于 composer 的机制自动加载类 require(__DIR__ .

玩转JS系列之代码加载篇

从前我们这样写js <script type="text/javascript"> function a(){ console.log('a init');}function b(){ console.log('b init'); a(); } </script> 随着功能越来越多,我们开始把js分离,使用单独的js文件来写,然后使用下面的方式引入js <script src="a.js" type="text/javascr