若转载教程,请注明出自SW-X框架官方文档

1、什么是自动加载?

在PHP5.0+之前,面向过程中,我们加载文件主要依靠include 和 require两个基本方法。
在小规模开发中直接使用 include 和 require 没什么不妥,但在大型项目中会造成大量的 include 和 require 堆积。
虽然可以运用单一入口+路由的管理方式,减少引入的次数,但项目逐渐壮大之后,
这样的代码既不优雅,执行效率也会很低,而且维护起来也相当困难。
为了解决这个问题,绝大部分的开源程序都会给出一个引入文件的配置清单,在对象初始化的时候把需要的文件引入。
但这只是让代码变得更简洁了一些,引入的效果仍然是差强人意。
PHP5.0+之后,随着PHP面向对象支持的完善,__autoload()系统函数才真正使得自动加载成为了可能。
自动加载,就是让我们可以按照一定的规则,自动引入对应的文件,达到惰性加载的效果。

2、初探__autoload()系统函数

__autoload()函数是PHP自定义的一个魔术方法,它默认是未创建状态,
我们创建了这个同名函数时,当需要使用的类没有被引入时,这个函数会在PHP报错前被触发,未定义的类名会被当作参数传入。
这时候我们就可以按照自己定义的规则,去自动加载某些文件。
例如:

  1. <?php
  2. function __autoload($class) {
  3. # 根据类名确定文件名
  4. $file = $class . '.php';
  5. # 判断文件是否存在
  6. if (file_exists($file)) {
  7. # 引入PHP文件
  8. include $file;
  9. }
  10. }
  11. new HelloWorld();

3、自定义函数,理解__autoload()的实现原理

实际上,我们可以通过自己编写一个自定义的函数,理解下,在类的实例化过程中,
__autoload()在系统中所做的工作大致是这样的

  1. <?php
  2. # 模拟系统执行过程
  3. function instance($class) {
  4. # 如果类存在则返回其实例
  5. if (class_exists($class, false)) {
  6. return new $class();
  7. }
  8. # 查看 __autoload 函数是否被用户定义
  9. if (function_exists('__autoload')) {
  10. # 最后一次引入的机会
  11. __autoload($class);
  12. }
  13. # 再次检查类是否存在
  14. if (class_exists($class, false)) {
  15. return new $class();
  16. } else {
  17. # 系统:我实在没辙了
  18. throw new Exception('Class所对应的文件不存在');
  19. }
  20. }
  21. function __autoload($class) {
  22. # 根据类名确定文件名
  23. $file = $class . '.php';
  24. # 判断文件是否存在
  25. if (file_exists($file)) {
  26. # 引入PHP文件
  27. include $file;
  28. }
  29. }
  30. $obj = instance('HelloWorld');

4、什么是命名空间?

其实命名空间并不是什么新生事物,很多语言(例如C++)中早都支持这个特性了。
只不过 PHP 起步比较晚,直到 PHP5.3+ 之后才支持。
命名空间简而言之就是一种标识,它的主要目的是解决命名冲突(类)的问题。
就好比上面我们所学到的自动加载,我们没办法识别出,当然new的类是/A/目录下的HelloWorld,
还是/B/目录下的HelloWorld,而按正常来说,我们是支持这不同目录下的同名。
而命名空间就是为了让自动加载,可以识别出文件对应所在的目录信息。

5、命名空间的声明方式

命名空间主要通过关键字 namespace 来声明。
如果一个文件中包含命名空间,那么它必须在其它所有代码之前声明命名空间(一般它都是首行代码)。
例如下面我们来举个例子,在Test/Order.php中写入以下代码:

  1. <?php
  2. namespace Test;
  3. class Order {
  4. public function __construct() {
  5. echo '我是Test\Order.php';
  6. }
  7. }

上面的代码中,namespace相当于单位标识(目录说明),class是姓名(类名)。
同时注意,namespace的声明是不区分大小写的,既namespace one;等于namespace ONE;

6、什么是顶级命名空间?

在PHP中,没有命名空间,就可以理解为默认是顶级命名空间。
new类时,可以在类前加上反斜杠\,也可以不加。
而在最左边加上\反斜杠的,都是顶级命名空间。
将Test/Order.php改成以下代码:

  1. <?php
  2. namespace Test;
  3. class Order {
  4. public function __construct() {
  5. echo '我是Test\Order.php';
  6. }
  7. }
  8. new \Test\Order(); // 访问的顶级命名空间(无错)
  9. new Order(); // 访问的当前命名空间(无错)
  10. new \Order(); // 访问的顶级命名空间(报错)

所以在new时使用命名空间,需要先考虑当前文件有没有声明namespace命名空间,如果有的话:
Test\Order 等于 当前命名空间\ + Test\Order;
如果没有的话:

  1. Test\Order 等于 \Test\Order

7、导入命名空间

朋友们先下载,我们的 DEMO压缩包 ,然后访问index.php文件,查看报错提示:

  1. Fatal error: Uncaught Error: Class 'Test\Order' not found in

意思是Test\Order类不存在
然后回过头,大家打开index.php文件,从代码中我们可以看出,
实际上我new的时候,已经使用了命名空间,但是却没能加载到对应的文件。
没错,在实际开发中,声明命名空间与使用命名空间,并不能自动加载到对应的类文件。
在PHP中,我们实例化类文件时,除了可以主动使用命名空间外,还支持提前导入需要使用到的命名空间。
导入空间主要通过关键字 use 来声明。
一个文件中可以导入多个命名空间,但是不能重复多次导入,它应该,但不是必须在其它所有代码之前导入命名空间(一般它都在namespace的下一行开始)。
案例代码可以将Demo中的index.php文件,修改成以下代码:

  1. <?php
  2. # 导入命名空间
  3. use Test\Order;
  4. new Order(); // 访问的顶级命名空间(报错)

同时,导入命名空间可能会存在一种情况,就是不同的路径但类名相同,这样就会导致new时,
会找不准到底是需要实例化哪一个类,
为了解决这种场景,use提供了AS关键字,导入命名空间时进行别名声明。
将index.php文件,修改成以下代码:

  1. <?php
  2. # 使用别名声明,导入命名空间
  3. use Test\Order AS Haha;
  4. new Haha(); // 访问的顶级命名空间(报错)

8、加载命名空间

上面的代码中,不管如何导入命名空间,系统仍然不会进行自动加载对应的文件。
如果不引入文件,系统会在抛出 “Class Not Found” 错误之前触发 __autoload() 函数,并将对应的类名传入作为参数。
所以上面的例子都是基于你已经将相关文件手动引入的情况下实现的,否则系统会抛出 “Class ‘Test\Order’ not found”的错误。
从上面我们可以看出,只需要主动引入对应的类文件,就能成功调用类。
将index.php文件,修改成以下代码:

  1. <?php
  2. # 主动引入对应的类文件
  3. require 'Test\Order.php';
  4. # 使用别名声明,导入命名空间
  5. use Test\Order AS Haha;
  6. new Haha(); // 访问的顶级命名空间(报错)

但如何我们主动去引入类文件,那么命名空间的作用就可以说是多余的了,所以这是一种错误的使用方法。
实际上,我们应该做的是,思考如何配合PHP面向对象,实现命名空间+自动加载机制。

9、自动加载命名空间

接下来,朋友们要在含有命名空间的情况下去实现自动加载对应的类文件。
PHP5.1.2+之后,提供了一个 spl_autoload_register() 函数来配合命名空间实现自动加载,这个函数的优先级要比autoload()函数高。
spl_autoload_register() 函数的作用就是把传入的函数(参数可以为回调函数或函数名称形式)注册到
autoload() 函数队列中,并移除系统默认的 autoload() 函数。
一旦调用 spl_autoload_register() 函数,当调用未定义类时,系统就会按顺序调用注册到 spl_autoload_register() 函数的所持函数中,而不是自动调用
autoload() 函数。
将index.php文件,修改成以下代码:

  1. ?php
  2. # 使用别名声明,导入命名空间
  3. use Test\Order AS Haha;
  4. # 定义一个类,用于处理自动加载
  5. function autoload_class($class) {
  6. # 输出命名空间看看
  7. echo $class;
  8. # 我们自己创建一个路由表-映射下
  9. $class_map = [
  10. # 命名空间 => 文件路径
  11. 'Test\Order' => 'Test/Order.php',
  12. ];
  13. # 根据命名空间找到对应文件路径
  14. $file = $class_map[$class];
  15. # 引入相关文件
  16. if (file_exists($file)) {
  17. include $file;
  18. }
  19. }
  20. # 注册自动加载监听
  21. spl_autoload_register('autoload_class');
  22. new Haha(); // 访问的顶级命名空间(报错)

上面的例子中,我们使用了一个路由表去保存命名空间与文件路径的关系,这样当命名空间传入时,自动加载器就知道该引入哪个文件去加载这个类了。
但是一旦命名空间多起来的话,映射数组会变得很长,这样的话维护起来会相当麻烦。
如果命名能遵守统一的约定,就可以让自动加载器自动解析对应的规范,判断类文件所在的路径。
接下来我们将要学习下PSR-4, 这是一种被广泛采用的自动加载规范。

10、PSR-4自动加载规范

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

\顶级命名空间\子级命名空间\类名

PSR-4 规范中必须要有一个顶级命名空间,它的意义在于表示某一个特殊的目录(文件根目录,但不代表项目根)。
子级命名空间代表的是类文件相对于文件根目录的这一段路径(相对路径),
类名则与文件名保持一致(注意区分大小写)。
举个例子:在顶级命名空间中 \app\view\news\Index 中,如果 app 代表 C:\Baidu,
那么这个类的路径则是 C:\Baidu\view\news\Index.php
我们就以解析 \app\view\news\Index 为例,编写一个简单的 Demo:

<?php
# 假设这是命名空间地址
$class = 'app\view\news\Index';
# 顶级命名空间路径映射
$vendor_map = [
    '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;

朋友们能否看出,如果基于上面的路径隐射表,我们是不是就能够扩展,并优化出更短的命名空间地址呢?

11、编写自动加载解析器

通过上面的 Demo 可以看出一个顶级命名空间转换为路径的过程其实非常简单。
那么现在就让我们用规范的面向对象方式去实现一个简单的自动加载器吧。
首先我们创建一个文件 Index.php,它处于 \app\view\home 目录中,代码如下:

<?php
namespace app\view\home;
class Index {
    public function __construct() {
        echo '我是app\view\home\Index.php';
    }
}
接着我们再创建一个自动加载类,这个类不需要使用命名空间,它处于 \ 目录中,命名为Loader.php:
<?php
class Loader {
    /** 
     * 命名空间-路径映射表 
     */
    public static $vendorMap = array(
        'app' => __DIR__ . DIRECTORY_SEPARATOR . 'app',
    );
    /**
     * 自动加载器
     * @param string $class 自动传入的命名空间路径
     */
    public static function autoload($class) {
        # 获得解析后的命名空间绝对路径
        $file = self::findFile($class);
        if (file_exists($file)) {
            # 调用引入
            self::includeFile($file);
        }
    }
    /**
     * 解析命名空间对应的文件路径
     * @param string $class 命名空间路径
     */
    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() 函数中,
\ 目录下新建一个index.php文件,并写入以下代码:

<?php
# 引入命名空间自动加载器
include 'Loader.php';
# 注册自动加载监听
spl_autoload_register('Loader::autoload'); 
# 导入命名空间
use app\view\home\Index;
# 实例化类
new Index();

上面的例子中使用了路由表的概念,朋友们还可以思考下,当如果路由表中查询不到注册的命名空间,该如何自动定位到未注册命名空间,所对应的绝对路径,因为大多数情况下,并不是所有命名空间的根目录都会注册到命名空间映射表中的。本案例Demo可以直接:下载