此篇文章同时发布在知乎专栏 前端后端客户端,专栏专注于前端、后端、客户端开发的技术分享与探讨,欢迎关注。

各位 PHPer 从入门到跑路精通的过程一般来说是这样的:

  1. 代码很简单,都堆在一个页面里
  2. 功能多了,开始按功能拆分代码,践行 OOP,使用各种 include()require() 来引入代码
  3. 学习使用框架,发现直接 use xxx 就可以很好地使用外部类了,这也太神奇了吧~!(破音尖叫

可能大家忙于惊呼神奇未曾探究框架是如何帮开发者实现外部文件加载的,今天我们就来具体谈谈 PHP 自动加载机制,以及诗意框架 Laravel 是如何实现它的。

为何需要自动加载

在没有自动加载机制前,我们使用外部类都需要手动使用 include()require() 进行文件引入,如果只是小项目的开发还能 hold 住,但一旦项目规模扩大就是一场噩梦:

  • 需要使用很多类时容易造成遗漏或引入不必要的文件
  • 若要避免重复引入需使用 require_once(),但 require_once() 的速度要比 require() 慢上 2-3 倍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Person.class.php */
<?php
class Person
{
var $name, $age;

function __construct ($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
?>

/* no_autoload.php */
<?php
require_once (”Person.class.php”);

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

为了解决上述问题,PHP 提供了一个解决方案:类的自动加载(autoload)机制

类的自动加载机制

__autoload()

在 PHP5 中,若我们使用一个没有加载的类,PHP 会自动运行 __autoload() 函数。因此,我们可以对 __autoload() 进行自定义,从而完成类的加载。

__autoload() 需要完成的功能有:

  1. 根据类名确定类文件名(需要约定映射规则)
  2. 确定类的具体路径(需要约定映射规则)
  3. 加载类(include()require() 的实现)

优点

  1. 摆脱长长的 include()require()
  2. 使用类时才会引入文件,实现了 lazy loading
  3. 无需知道类的实际文件地址,实现了逻辑和实体文件的分离

存在问题

__autoload() 好处多多,但也同样存在着问题:

  1. 是全局函数,只能定义一次,不够灵活
  2. 类名和文件名的映射规则可能各不相同,都在一个函数中实现造成函数臃肿混乱

解决上述问题,我们需要将不同的映射关系写到不同的 __autoload() 中去,再进行统一的注册和管理。

因此,SPL Autoload 系列函数就出现了,它帮助我们使用 autoload 调用栈。

SPL Autoload

函数列表与使用方式具体见 SPL Functions

类名与文件映射规则

命名空间

映射规范

包管理工具 Composer

使用 PHP 自动加载 + PSR4 标准 我们就可以自己实现一套自动加载程序了。但有了 Composer 这款包管理神器,我们也无需自己动手了。

Composer 可以帮助解决以下问题:

  • 项目依赖若干个库
  • 其中一些库依赖于其他库
  • 声明所依赖的东西
  • 根据版本查找需要安装的包并安装它

自动加载文件

  1. autoload_real.php:自动加载功能的引导类,负责 composer 加载类的初始化和注册
  2. ClassLoader.php:composer 加载类,自动加载功能的核心类
  3. autoload_static.php:顶级命名空间初始化类,用于给核心类初始化顶级命名空间
  4. autoload_classmap.php: 自动加载的最简单形式,有完整的命名空间和文件目录的映射
  5. autoload_files.php: 用于加载全局函数的文件,存放各个全局函数所在的文件路径名
  6. autoload_namespaces.php: 符合PSR0标准的自动加载文件,存放着顶级命名空间与文件的映射
  7. autoload_psr4.php: 符合PSR4标准的自动加载文件,存放着顶级命名空间与文件的映射

Laravel 的具体实现

从入口文件出发

我们从 Laravel 的入口文件 public/index.php 看起:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/

require __DIR__.'/../vendor/autoload.php'; // 注册了自动加载机制

前往 autoload.php

1
2
3
4
5
// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php'; // 自动加载功能的引导类

return ComposerAutoloaderInit1cf4a5d9084e6125482a4af4d90171ad::getLoader();

在这里我们看到引导类的名字是 ComposerAutoloaderInit1cf4a5d9084e6125482a4af4d90171ad。Laravel 为了防止用户定义的类名和这个类重复冲突,所以在类名上加上了哈希值。

看看引导类

在上述 autoload.php 文件中调用了引导类的静态方法 getLoader()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInit1cf4a5d9084e6125482a4af4d90171ad
{
private static $loader;

public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
// 这里包含了 ClassLoader 文件
require __DIR__ . '/ClassLoader.php';
}
}

public static function getLoader()
{
// 单例模式,只能有一个类的实例
if (null !== self::$loader) {
return self::$loader;
}

// 向 PHP 自动加载机制注册了一个函数
spl_autoload_register(array('ComposerAutoloaderInit1cf4a5d9084e6125482a4af4d90171ad', 'loadClassLoader'), true, true); // spl_autoload_register — 注册给定的函数作为 __autoload 的实现
// new 出该文件中核心类 ClassLoader()
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
// 销毁该函数
spl_autoload_unregister(array('ComposerAutoloaderInit1cf4a5d9084e6125482a4af4d90171ad', 'loadClassLoader'));

// 初始化自动加载核心对象类,给自动加载核心类初始化顶级命名空间映射
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
// 静态初始化,仅支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机
require_once __DIR__ . '/autoload_static.php';

call_user_func(\Composer\Autoload\ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad::getInitializer($loader));
} else {
// 如果 PHP 版本低于 5.6 或者使用 HHVM 虚拟机环境,调用核心类接口初始化

// PSR0 标准
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}

// PSR4 标准
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}

// 傻瓜映射法
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}

$loader->register(true);

// 引入了 autoload_files,包含的数组内容会提前加载,而不是用到时才加载
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire1cf4a5d9084e6125482a4af4d90171ad($fileIdentifier, $file);
}

// $loader 被返回
return $loader;
}
}

function composerRequire1cf4a5d9084e6125482a4af4d90171ad($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;

$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
  • autoload_classmap.phpautoload_namespaces.phpautoload_psr4.php 所包含的数组内容来自于 composer.json

静态初始化文件 autoload_static.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace Composer\Autoload;

class ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad
{
// ......

// 将自己类中的顶级命名空间映射给了 ClassLoader 类
public static function getInitializer(ClassLoader $loader)
{
// 返回了一个匿名函数
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad::$prefixesPsr0;
$loader->classMap = ComposerStaticInit1cf4a5d9084e6125482a4af4d90171ad::$classMap;

}, null, ClassLoader::class);
}
}

getInitializer 返回了一个匿名函数,这是因为 ClassLoader 中的 prefixLengthsPsr4 等方法都是私有的,普通函数无法给类的私有成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 ClassLoader 类的成员函数。

⚠️注:

这里涉及到 PHP 匿名函数的绑定功能,详见:PHP 中的Closure

总结

  • 自动加载机制避免了到处 include/require 的糟糕写法
  • 当使用没有加载的类时,会自动运行 __autoload()
  • __autoload() 全局只能定义一次,因此我们需要使用 autoload 调用栈,通过注册不同的 __autoload 来实现不同的映射规则。SPL Autoload 系列函数可以帮助我们实现这些
  • 命名空间大法好
  • Composer 是个神奇的包管理工具
  • 遵循规范能帮助我们更好地完成开发任务:Moving PHP forward through collaboration and standards
  • Laravel:The PHP Framework For Web Artisans

参考资料