文档章节

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

一配
 一配
发布于 2015/07/29 12:08
字数 2707
阅读 3913
收藏 16

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/

© 著作权归作者所有

共有 人打赏支持
一配
粉丝 34
博文 137
码字总数 94295
作品 0
西城
私信 提问
FIG-PHP PSR规范系列1-基础编码规范

FIG-PHP与PSR简介 FIG-PHP制定了一系列PHP开发规范,简称PSR,这里FIG是框架互操作工作组(Framework Interoperability Group)的简称,PSR是PHP标准推荐(PHP Standard Recommendation)的缩...

一配
2015/07/29
0
0
PHP编码规范(PSR-4)-Autoloader 自动加载

PSR-4 Autoloader 1. 概述 本 PSR 是关于由文件路径 自动载入 对应类的相关规范, 本规范是可互操作的,可以作为任一自动载入规范的补充,其中包括 PSR-0,此外, 本 PSR 还包括自动载入的类...

2688
2016/07/04
57
0
PHP编码规范(PSR)的简述

PSR简介 PSR是一系列关于PHP开发的规范,一般大家遵守的是 php-fig定义的规范,中文翻译版本参考目前官方已制定的规范包括以下六份文件: PSR-0 (弃用) PSR-1 (基本代码规范) PSR-2 (代码风格...

2688
2016/07/04
335
0
利用 Composer 一步一步构建自己的 PHP 框架(一)——基础准备

“一个时代结束了,另一个时代开始了。” Framework Interoperability Group(框架可互用性小组),简称 FIG,成立于 2009 年。FIG 最初由几位知名 PHP 框架开发者发起,在吸纳了许多优秀的大...

peasant
2016/06/03
11
0
利用 Composer 一步一步构建自己的 PHP 框架(一)——基础准备

利用 Composer 一步一步构建自己的 PHP 框架(一)——基础准备 『Composer 一统天下的时代已经到来!』——白岩松 “一个时代结束了,另一个时代开始了。” Framework Interoperability Gro...

蜗牛奔跑
2016/10/15
43
0

没有更多内容

加载失败,请刷新页面

加载更多

Vert.x系列(二)--EventBusImpl源码分析

前言:Vert.x 实现了2种完成不同的eventBus: EventBusImpl(A local event bus implementation)和 它的子类 ClusteredEventBus(An event bus implementation that clusters with other Ve......

冷基
53分钟前
1
0
Perl - 获取文件项目

参考:http://www.runoob.com/perl/perl-directories.html 下面返回JSON格式的文件列表 #!/usr/bin/perluse strict;use warnings;use utf8;use feature ':5.26';require Fi......

wffger
昨天
2
0
vue组件系列3、查询下载

直接源码,虽然样式样式不好看,逻辑也不是最优,但是可以留作纪念。毕竟以后类似的功能只需要优化就可以了,不用每次都重头开始。。。 <template> <div class="pre_upload"> <div ...

轻轻的往前走
昨天
2
0
java浅复制和深复制

之前写了数组的复制,所以这里继续总结一下浅复制和深复制。 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝。 深拷贝:对基本数据类型进行值传递,对引用数据类型,...

woshixin
昨天
2
0
kubernetes 二进制包安装

环境 角色 主机名 内网 IP 集群 IP 操作系统 服务 执行目录 部署机 k8s-master master120 10.0.4.120 - CentOS kube-apiserver kube-scheduler kube-controller-manager /opt/kubernetes/ et......

Colben
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部