PHP新特性 第1章 命名空间与自动加载机制 PHP新特性 第1章 命名空间与自动加载机制

2019-08-24

一、自动加载

实现自动加载最简单的方式就是使用 __autoload 魔术方法。当需要使用的类没有被引入时,这个函数会在PHP报错前被触发,未定义的类名会被当作参数传入。至于函数具体的逻辑,这需要用户自己去实现。

首先创建一个 autoload.php 来做一个简单的测试:

// 类未定义时,系统自动调用
function __autoload($class)
{
    /* 具体处理逻辑 */
    echo $class;// 简单的输出未定义的类名
}

new HelloWorld();

/**
 * 输出 HelloWorld 与报错信息
 * Fatal error: Class 'HelloWorld' not found
 */

通过这个简单的例子可以发现,在类的实例化过程中,系统所做的工作大致是这样的:

/* 模拟系统实例化过程 */
function instance($class)
{
    // 如果类存在则返回其实例
    if (class_exists($class, false)) {
        return new $class();
    }
    // 查看 autoload 函数是否被用户定义
    if (function_exists('__autoload')) {
        __autoload($class); // 最后一次引入的机会
    }
    // 再次检查类是否存在
    if (class_exists($class, false)) {
        return new $class();
    } else { // 系统:我实在没辙了
        throw new Exception('Class Not Found');
    }
}

明白了 __autoload 函数的工作原理之后,那就让我们来用它去实现自动加载。

首先创建一个类文件(建议文件名与类名一致),代码如下:

class [ClassName] 
{
    // 对象实例化时输出当前类名
    function __construct()
    {
        echo '' . __CLASS__ . '';
    }
}

(我这里创建了一个 HelloWorld 类用作演示)接下来我们就要定义 __autoload 的具体逻辑,使它能够实现自动加载:

function __autoload($class)
{
    // 根据类名确定文件名
    $file = $class . '.php';

    if (file_exists($file)) {
        include $file; // 引入PHP文件
    }
}

new HelloWorld();

/**
 * 输出 HelloWorld */

二、php 命名空间

为解决引用相同类名而使用的一种机制

其实命名空间并不是什么新生事物,很多语言(例如C++)早都支持这个特性了。只不过 PHP 起步比较晚,直到 PHP 5.3 之后才支持。

命名空间简而言之就是一种标识,它的主要目的是解决命名冲突的问题。

就像在日常生活中,有很多姓名相同的人,如何区分这些人呢?那就需要加上一些额外的标识。

把工作单位当成标识似乎不错,这样就不用担心 “撞名” 的尴尬了。

这里我们来做一个小任务,去介绍百度的CEO李彦宏:

namespace 百度;

class 李彦宏
{
    function __construct()
    {
        echo '百度创始人';
    }
}

↑ 这就是李彦宏的基本资料了,namespace 是他的单位标识,class 是他的姓名。

命名空间通过关键字 namespace 来声明。如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间。

new 百度\李彦宏(); // 限定类名
new \百度\李彦宏(); // 完全限定类名

↑ 在一般情况下,无论是向别人介绍 "百度 李彦宏" 还是 "百度公司 李彦宏",他们都能够明白。

在当前命名空间没有声明的情况下,限定类名和完全限定类名是等价的。因为如果不指定空间,则默认为全局(\)。

namespace 谷歌;

new 百度\李彦宏(); // 谷歌\百度\李彦宏(实际结果)
new \百度\李彦宏(); // 百度\李彦宏(实际结果)

↑ 如果你在谷歌公司向他们的员工介绍李彦宏,一定要指明是 "百度公司的李彦宏"。否则他会认为百度是谷歌的一个部门,而李彦宏只是其中的一位员工而已。

这个例子展示了在命名空间下,使用限定类名和完全限定类名的区别。(完全限定类名 = 当前命名空间 + 限定类名)

/* 导入命名空间 */
use 百度\李彦宏;
new 李彦宏(); // 百度\李彦宏(实际结果)

/* 设置别名 */
use 百度\李彦宏 AS CEO;
new CEO(); // 百度\李彦宏(实际结果)

/* 任何情况 */
new \百度\李彦宏();// 百度\李彦宏(实际结果)

↑ 第一种情况是别人已经认识李彦宏了,你只需要直接说名字,他就能知道你指的是谁。第二种情况是李彦宏就是他们的CEO,你直接说CEO,他可以立刻反应过来。

使用命名空间只是让类名有了前缀,不容易发生冲突,系统仍然不会进行自动导入。

如果不引入文件,系统会在抛出 "Class Not Found" 错误之前触发 __autoload 函数,并将限定类名传入作为参数。

所以上面的例子都是基于你已经将相关文件手动引入的情况下实现的,否则系统会抛出 " Class '百度\李彦宏' not found"。

三、spl_autoload

接下来让我们要在含有命名空间的情况下去实现自动加载。这里我们使用 spl_autoload_register() 函数来实现,这需要你的 PHP 版本号大于 5.12。

spl_autoload_register 函数的功能就是把传入的函数(参数可以为回调函数或函数名称形式)注册到 SPL __autoload 函数队列中,并移除系统默认的 __autoload() 函数。

一旦调用 spl_autoload_register() 函数,当调用未定义类时,系统就会按顺序调用注册到 spl_autoload_register() 函数的所有函数,而不是自动调用 __autoload() 函数。

现在,我们来创建一个 Linux 类,它使用 os 作为它的命名空间(建议文件名与类名保持一致):

namespace os; // 命名空间

class Linux // 类名
{
    function __construct()
    {
        echo '' . __CLASS__ . '';
    }
}

接着,在同一个目录下新建一个 PHP 文件,使用 spl_autoload_register 以函数回调的方式实现自动加载:

spl_autoload_register(function ($class) { // class = os\Linux

    /* 限定类名路径映射 */
    $class_map = array(
        // 限定类名 => 文件路径
        'os\\Linux' => './Linux.php',
    );

    /* 根据类名确定文件名 */
    $file = $class_map[$class];

    /* 引入相关文件 */
    if (file_exists($file)) {
        include $file;
    }
});

new \os\Linux();

这里我们使用了一个数组去保存类名与文件路径的关系,这样当类名传入时,自动加载器就知道该引入哪个文件去加载这个类了。

但是一旦文件多起来的话,映射数组会变得很长,这样的话维护起来会相当麻烦。如果命名能遵守统一的约定,就可以让自动加载器自动解析判断类文件所在的路径。接下来要介绍的PSR-4 就是一种被广泛采用的约定方式。

四、PSR-4规范

PSR-4 是关于由文件路径自动载入对应类的相关规范,规范规定了一个完全限定类名需要具有以下结构:

\(\)*\

如果继续拿上面的例子打比方的话,顶级命名空间相当于公司,子命名空间相当于职位,类名相当于人名。那么李彦宏标准的称呼为 "百度公司 CEO 李彦宏"。


PSR-4 规范中必须要有一个顶级命名空间,它的意义在于表示某一个特殊的目录(文件基目录)。子命名空间代表的是类文件相对于文件基目录的这一段路径(相对路径),类名则与文件名保持一致(注意大小写的区别)。

举个例子:在全限定类名 \app\view\news\Index 中,如果 app 代表 C:\Baidu,那么这个类的路径则是 C:\Baidu\view\news\Index.php

我们就以解析 \app\view\news\Index 为例,编写一个简单的 Demo:

$class = 'app\view\news\Index';

/* 顶级命名空间路径映射 */
$vendor_map = array(
    'app' => 'C:\Baidu',
);

/* 解析类名为文件路径 */
$vendor = substr($class, 0, strpos($class, '\\')); // 取出顶级命名空间[app]
$vendor_dir = $vendor_map[$vendor]; // 文件基目录[C:\Baidu]
$rel_path = dirname(substr($class, strlen($vendor))); // 相对路径[/view/news]
$file_name = basename($class) . '.php'; // 文件名[Index.php]

/* 输出文件所在路径 */
echo $vendor_dir . $rel_path . DIRECTORY_SEPARATOR . $file_name;

通过这个 Demo 可以看出限定类名转换为路径的过程。那么现在就让我们用规范的面向对象方式去实现自动加载器吧。

首先我们创建一个文件 Index.php,它处于 \app\mvc\view\home 目录中:

namespace app\mvc\view\home;

class Index
{
    function __construct()
    {
        echo ' Welcome To Home ';
    }
}

接着我们在创建一个加载类(不需要命名空间),它处于 \ 目录中:

class Loader
{
    /* 路径映射 */
    public static $vendorMap = array(
        'app' => __DIR__ . DIRECTORY_SEPARATOR . 'app',
    );

    /**
     * 自动加载器
     */
    public static function autoload($class)
    {
        $file = self::findFile($class);
        if (file_exists($file)) {
            self::includeFile($file);
        }
    }

    /**
     * 解析文件路径
     */
    private static function findFile($class)
    {
        $vendor = substr($class, 0, strpos($class, '\\')); // 顶级命名空间
        $vendorDir = self::$vendorMap[$vendor]; // 文件基目录
        $filePath = substr($class, strlen($vendor)) . '.php'; // 文件相对路径
        return strtr($vendorDir . $filePath, '\\', DIRECTORY_SEPARATOR); // 文件标准路径
    }

    /**
     * 引入文件
     */
    private static function includeFile($file)
    {
        if (is_file($file)) {
            include $file;
        }
    }
}

最后,将 Loader 类中的 autoload 注册到 spl_autoload_register 函数中:

include 'Loader.php'; // 引入加载器
spl_autoload_register('Loader::autoload'); // 注册自动加载

new \app\mvc\view\home\Index(); // 实例化未引用的类

/**
 * 输出:  Welcome To Home  */

示例中的代码其实就是 ThinkPHP 自动加载器源码的精简版,它是 ThinkPHP 5 能实现惰性加载的关键。

至此,自动加载的原理已经全部讲完了,如果有兴趣深入了解的话,可以参考下面的 ThinkPHP 源码。

五、PHP PSR 代码规范基本介绍

PSR 是 PHP Standard Recommendation 的简写,即PHP推荐标准。

目前通过的规范有 PSR-0(Autoloading Standard)、PSR-1(Basic Coding Standard)、PSR-2(Coding Style Guide)、PSR-3(Logger Interface)、PSR-4(Improved Autoloading)。

PSR 不是PHP官方标准,而是从如Zend、Symfony2等知名PHP项目中提炼出来的一系列标准,目前有越来越多的社区项目加入并遵循该标准。

PHP FIG(Framework Interoperability Group)框架可互用性小组是制定PSR开发规范的组织。他们的目的在于以最低程度的限制制定一个统一的标准,让各个框架遵循统一的编码规范。

①、PSR-0(自动加载规范)

PSR-0(Autoloading Standard)类自动加载规范,该规范现已废弃(Deprecated),它将由PSR-4替代。

  • 一个完全合格的命名空间和类名必须遵循以下结构 "\VendorName\Namespace\ClassName"

  • 每个命名空间必须有顶级的命名空间 "VendorName"

  • 每个命名空间可以有任意多个子命名空间

  • 每个命名空间在被文件系统加载时必须被转换为操作系统路径分隔符 (DIRECTORY_SEPARATOR)

  • 每个"_"字符在"类名"中被转换为DIRECTORY_SEPARATOR。而在 PSR-4 中使用下划线没有任何特殊含义

  • 符合命名标准的命名空间和类名必须以".php"结尾来加载文件

  • 命名空间和类名可以由大小写字母组成,但必须对大小写敏感以保证多系统兼容性

②、PSR-1(基本代码规范)

PSR-1(Basic Coding Standard)基本代码规范,用以确保共享的PHP代码间具有较高程度的技术互通性。

  • PHP代码源文件必须以 <?php 或 <?= 标签开始

  • PHP代码源文件必须使用不带 BOM 的 UTF-8 编码

  • 一个源文件建议只用作定义类、函数、常量等声明,或者其他产生从属效应的操作(如:输出信息,修改配置文件等)

  • 命名空间以及类必须符合 PSR 的自动加载规范:PSR-0 或 PSR-4

  • 类的命名必须遵循 StudlyCaps 大写开头的驼峰式命名规范

  • 类中的常量所有字母都必须大写,单词间用下划线分隔

  • 方法名必须符合 camelCase 式的小写开头驼峰式命名规范

BOM(byte order mark)是 Unicode 标准的一部分,通常用于标记纯文本字节序(byte order),使得文本处理程序能够识别读入的文件使用的 Unicode 编码(UTF-8、UTF-16、UTF-32)。

从属效应是指仅仅通过包含文件,不直接声明类、函数和常量而执行的逻辑操作。一份PHP源文件应该要么就只包含不产生从属效应的定义操作,要么就包含只会产生从属效应的逻辑操作,切勿同时包含两者。

③、PSR-2(代码风格规范)

PSR-2(Coding Style Guide)代码风格规范,通过制定一系列规范化PHP代码的规则,以减少因代作者码风格不同而造成的阅读不便。

  • 代码必须遵循 PSR-1 中的编码规范

  • 代码必须使用4个空格来进行缩进,而非制表符(TAB)

  • 建议每行代码字符数保持在80个以内,理论上不可多于120个,但不做硬性限制

  • 每个 namespace 命名空间语句和 use 声明语句块后面必须插入一个空白行

  • 类的左花括号 "{" 必须写在声明后自成一行,右花括号 "}" 也必须在类主体下自成一行

  • 方法的左花括号 "{" 必须放在声明后自成一行,右花括号 "}" 也必须于主体下自成一行

  • 类的属性和方法必须添加访问修饰符(private、protected、public),abstract 以及 final 必须声明在访问修饰符之前,而 static 必须声明在访问修饰符之后(例:final public static)

  • 在控制结构关键字的后面必须有一个空格,而调用方法或函数时一定不能有(控制结构:if-else、switch-case、try-catch、while、foreach ...)

  • 控制结构的左花括号 "{" 必须跟其处于同一行,右花括号 "}" 必须在控制结构主体之后自成一行

  • 控制结构的开始左括号之后,和结束右括号之前都不可以有空格

④、PSR-3(日志接口规范)

PSR-3(Logger Interface)日志接口规范,主要目的是为了让日志类库通过接收一个 LoggerInterface 对象来记录日志信息。

  • LoggerInterface 接口对外定义了八个方法,分别用来记录 RFC 5424 中定义的八个等级的日志:debug、info、notice、warning、error、critical、alert、emergency

  • 第九个方法 log(),第一个参数为记录等级。可使用一个预先定义的等级常量作为参数来调用此方法,必须与直接调用以上八个方法具有相同的效果。如果传入的等级常量没有预先定义,则必须抛出 psr\Log\InvalidArgumentException 类型的异常。不推荐使用自定义的日志等级,除非你非常确定当前类库对其有所支持。

⑤、PSR-4(自动加载新规)

PSR-4(Improved Autoloading)本规范是关于自动载入对应类的相关规范,是 PSR-0 自动加载规范的补充。

A.此处的“类”是一个泛称,它包含类、接口、traits 以及其他类似的结构

B.完全限定类名需要遵循以下结构:\<命名空间>(\<子命名空间>)*\<类名>

  • 完全限定类名必须要有一个顶级命名空间,被称为 "vendor namespace";

  • 完全限定类名可以有一个或多个子命名空间;

  • 完全限定类名必须有一个终止类名;

  • 完全限定类名中任意一部分中的下划线都没有特殊含义;

  • 完全限定类名可以由任意大小写字母组成;

  • 完全限定类名必须以大小写敏感的方式引用;

C.当根据完整的类名载入相应的文件时:

  • 完全限定类名中,连续的一个或几个子命名空间构成的命名空间前缀(不包括顶级命名空间的分隔符),至少对应着至少一个基础目录;

  • 紧接命名空间前缀后的子命名空间必须与相应的”文件基目录“相匹配,其中的命名空间分隔符将作为目录分隔符;

  • 终止类名对应一个以 .php 结尾的文件,文件名必须和终止类名大小写匹配;

D.自动加载器(autoloader)的实现不能抛出异常,不可引发任一级别错误,也不应该有返回值

阅读 1894