PHP 命名空间与类自动加载实现

从文件引入谈起

在 PHP 5.3 之前,要在一个 PHP 脚本中引入另一个 PHP 脚本中定义的代码(通常是函数或者类),需要借助 includerequireinclude_oncerequire_once 等语句,include 和 require 都可以通过指定路径引入一个 PHP 脚本,区别是 include 没有找到对应路径脚本时发出警告(E_WARNING),而 require 会抛出致命错误(E_COMPILE_ERROR),include_once/require_once 也是用于引入指定路径 PHP 脚本,与 include/require 的区别是如果指定路径已经包含过,不会再次包含,换言之,只会包含一次同一路径脚本,include_once 和 require_once 的区别与 include/require 一样。

所以从性能角度说,使用 include_once/require_once 性能更好一些,至于使用 include_once 还是 require_once,取决于你对指定路径 PHP 脚本不存在的预期处理。

在前面的作业中,我们已经多次使用过它们来引入其他 PHP 脚本文件,比如在博客项目入口文件 index.php 中,我们通过如下代码引入 bootstrap.php 以便引入初始化函数 bootApp 进行调用:

<?php
require_once 'bootstrap.php';

// 新增一个 IoC 容器,通过依赖注入获取对象实例
$container = Container::getInstance();
bootApp($container);

...

然后在 bootstrap.php 中,又通过如下代码引入 Container 类定义:

<?php
require_once 'core/Container.php';

...

自动加载类文件

对于类文件的引入,如果你觉得反复编写 require_once/include_once 语句太麻烦,还可以借助 spl_auto_register 函数注册自动加载器,实现系统未定义类或接口的自动加载。

比如我们将上述 bootstrap.php 中的通过 require_once 引入 Container 类代码调整为通过 spl_autoload_register 函数自动注册:

spl_autoload_register(function ($className) {
    require_once 'core/' . $className. '.php';
});

这样,我们只需要通过 spl_autoload_register 全局注册这个匿名函数即可,当 Container 类找不到时,会根据这个自动加载器进行加载。

命名空间及其使用

结合 require_once/include_oncespl_autoload_register,已经可以很好地解决多个 PHP 脚本之间引入和组合的问题,从而构建出复杂系统,比如 Web 开发框架,或者第三方库等,事实上,在 PHP 5.3 之前,第三方框架和库就是这么做的,不过,细心的同学可能已经看出来,spl_autoload_register 这种自动类加载机制存在一个问题,那就是不同库/组件类名冲突问题,因此,从 PHP 5.3 开始,引入了命名空间的概念,通过命名空间,可以很好的解决这个问题,而且相较于前者,代码可读性更好。

在 PHP 中,通过 namespace 关键字声明当前脚本所在的命名空间,通常,一个 PHP 脚本文件归属于一个命名空间。我们在 php_learning 目录下新建一个 ns 子目录存放本篇教程代码,然后在 ns 目录下创建一个 Test.php 文件,编写一段简单的测试代码如下:

<?php
namespace App;

class Test
{
    public static function print ()
    {
        printf("这是一个测试类: %s\n", __CLASS__);
    }
}

我们需要在 PHP 脚本的第一行代码声明代码所属的命名空间(必须是第一行,否则会报错):

namespace App;

表明这段脚本中的所有 PHP 常量、变量、类、函数都归属于这个命名空间,然后我们在这个命名空间中声明了一个 Test 类,以及一个静态方法 print 来打印类名。

接下来,我们在同一目录下创建一个 App.php 脚本来调用 Test::print() 方法:

-w690

App.phpTest.php 归属于同一个目录,所以声明了相同的命名空间,实际开发过程中,我们通常就是根据目录来组织并管理命名空间的。调用同一个命名空间中的类和函数,可以像上面代码这样直接调用,如果是不同命名空间的类和函数,则需要通过 use 关键字引入,我们在 ns 目录下新建一个 testing 子目录,并在该子目录下新建一个 Test.php,在这个 PHP 脚本中,我们定义了一个继承自上级目录中定义的 Test 父类的同名子类:

-w1085

这里,我们将该子类所属命名空间声明为 App\Testing(同一个命名空间下不允许出现重名的类和函数),然后通过 use 关键字引入上级命名空间中的 Test 类,由于该类名与子类名同名,所以通过 as 关键字为其设置一个别名 BaseTest,接下来,就可以通过 BaseTest 引用 Test 父类。

Test 子类中,我们重写了父类 BaseTestprint 方法。

最后,我们可以在 App.php 中这样调用这个子类:

<?php
namespace App;
use App\Testing\Test as SubTest;

Test::print();
SubTest::print();

如果不存在类名冲突,则不需要设置别名:

<?php
namespace App;
use App\Testing\Test;

Test::print();

此外,还可以不使用 use 关键字,直接引用包含完整命名空间的类名:

<?php
namespace App;

Test::print();
\App\Testing\Test::print();

或者这样,使用部分命名空间:

<?php
namespace App;
use App\Testing;

Test::print();
Testing\Test::print();

但是,我们这个系列教程约定通过 use 引入完整命名空间,以避免代码的冗长,提高可读性。

注:学院君这里只是抛砖引玉,简单介绍了 PHP 命名空间的基本使用,更多细节请参考官方文档 或者现代 PHP 新特性系列(一) —— 命名空间这篇教程。

自动加载命名空间类

当然,现在调用 php App.php 会报错,不论是 App\Test 还是 App\Testing\Test 类都提示找不到:

-w999

-w1016

要解决这个问题,可以借助上面提到的 spl_autoload_register 函数,将类名所属命名空间解析为对应的目录路径(这就是为什么要根据目录来组织命名空间),然后把通过 require_once/include_once 引入,我们在 App.php 中加入如下这段代码:

<?php
namespace App;
use App\Testing\Test as SubTest;

spl_autoload_register(function ($className) {
    $path = explode('\\', $className);
    if ($path[0] == 'App') {
        $base = __DIR__;
    }
    $filename = $path[count($path) - 1] . '.php';
    $filepath = $base;
    foreach ($path as $key => $val)
    {
        if ($key == 0 || $key == count($path) - 1) {
            continue;
        }
        $filepath .= DIRECTORY_SEPARATOR . strtolower($val);
    }
    $filepath .= DIRECTORY_SEPARATOR . $filename;
    require_once $filepath;
});

Test::print();
SubTest::print();

这样,我们就可以正常调用这段代码了:

-w697

通过 Composer 管理命名空间

实际项目开发时,手动编写这段 spl_autoload_register 代码有点麻烦,尤其是项目除了自己编写的代码外,还要引入各种第三方库,我们可以借助 PHP 的包管理工具 Composer 帮我们管理这种命名空间与目录路径的映射,在此之前,我们已经在 PHP 环境搭建篇中在本地系统中安装好了 Composer,因此,只需要在 ns 目录下运行 composer init 初始化 Composer 设置即可,按照向导一路往下走即可,最后会在项目根目录下生成一个 composer.json 配置文件:

-w834

如果项目有第三方库依赖,可以在 require 中进行配置,这里是一个测试项目,暂时还没有任何依赖,然后我们在其中配置 autoload 选项来设置类自动加载机制:

{
    "name": "php/test",
    "description": "A php namespace test project",
    "type": "project",
    "license": "Apache",
    "authors": [
        {
            "name": "xueyuanjun",
            "email": "yaojinbu@outlook.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "classmap": [
            "."
        ]
    }
}

这里,我们通过在 classmap 数组中添加 . 表示当前根目录作为类自动加载的入口目录,Composer 会从这里开始读取所有命名空间并建立目录映射关系。接下来执行 composer install 初始化依赖库和类自动加载设置:

-w1238

初始化过程中,会在根目录下创建 vendor 用来存放第三方依赖包和类自动加载相关文件。初始化完成后,可以看到 vendor/composer/autoload_static.php 中已经包含了 App 及其子命名空间的目录映射了:

-w1024

该文件会被 autoload_real.php 引用,autoload_real.php 又会被 vendor/autoload.php 引用:

<?php

// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInit991075cd5c3b2a6d389bb443802f7669::getLoader();

autoload_php 是所有 Composer 管理类自动加载的入口文件,所以我们只需要在代码中引入这个文件即可通过 Composer 来管理所有类的自动加载,在 App.php 中,修改示例代码如下:

<?php
include_once 'vendor/autoload.php';

use App\Test;
use App\Testing\Test as SubTest;

Test::print();
SubTest::print();

比起之前手动编写 spl_autoload_register 进行类自动加载,现在的代码更加简单清晰,执行 php App.php,运行结果如下:

-w818

实际上,Composer 底层也是通过 spl_autoload_register 函数实现类的自动加载的,只是在此之前,还会建立命令空间与类脚本路径的映射,更多细节,可以参考 Laravel 框架如何基于 Composer 实现类和文件的自动加载 这篇教程,当然,Composer 作为 PHP 的包管理工具,其功能远不止于此,其更强大的功能在于对第三方扩展包和库进行安装、维护和管理,限于篇幅,这里就不详细展开了,感兴趣的同学可以参考以下两篇教程:

综上,有了命令空间和 Composer 加持,我们可以基于 PHP 轻松构建和维护复杂的、现代的大型项目,下篇教程开始,学院君将给大家演示如何从零开始构建一个 PHP Web 框架。

上一篇: 在 PHP 中使用和管理 Session 并实现简单的用户登录功能

下一篇: 基于 Symfony 组件封装 HTTP 请求响应类