类的自动加载是框架中非常重要的特性,它允许你在使用类时无需手动包含或引入对应的文件。类的自动加载实现起来很简单,只需这样的一个函数spl_autoload_register
就能实现。但框架都有各自的加载规范,并不是所有类都能被自动加载,因此这节内容大家还可以了解到PSR-4
的自动加载规范,另外也可以弄明白通过composer引入进来的类是如何被加载的。
带着我们的好奇心开始我们thinkphp
源码之旅,打开入口文件public/index.php
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);
第一行代码就是载入composer的自动加载文件autoload.php
,实现类的自动加载。
我们接下来重点研究一下autoload.php
,在vender
目录下可以找到该文件
if (PHP_VERSION_ID < 50600) {
/**版本相关的限制,省略代码**/
}
// 引入autoload_real.php,这个类是由composer自动生产的
require_once __DIR__ . '/composer/autoload_real.php';
// 调用该类里面的getLoader方法,这个类名有点长,这也是composer自动生成的
return ComposerAutoloaderInit71a72e019c6be19dac67146f3f5fb8de::getLoader();
接下来看看getLoader()
方法做了什么
public static function getLoader()
{
// 如果$loader不为空,说明已经经过一些列的初始化了,就直接返回了
if (null !== self::$loader) {
return self::$loader;
}
// php版本相关的检查,这里就不细讲
require __DIR__ . '/platform_check.php';
// spl_autoload_register这个函数很重要,后面类的自动加载就是用这个函数,这里先给大家预热一波,这个函 // 数的用法,具体的大家可以看看文档:https://www.php.net/manual/zh/function.spl-autoload- register
// 这行代码的意思就是把当前类里的loadClassLoader函数作为__autoload 的实现,
/*
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
// 其实就是引入当ClassLoader类
require __DIR__ . '/ClassLoader.php';
}
}
*/
spl_autoload_register(array('ComposerAutoloaderInit71a72e019c6be19dac67146f3f5fb8de', 'loadClassLoader'), true, true);
// new ClassLoader的时候,会自动执行前面装载的函数loadClassLoader,引入ClassLoader.php
// 这样就实现了类的自动加载(引入)
// 问题1:为什么这里不使用require直接引入,这样不是更简单一些吗??
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
// 这里是删除loadClassLoader,释放资源
spl_autoload_unregister(array('ComposerAutoloaderInit71a72e019c6be19dac67146f3f5fb8de', 'loadClassLoader'));
// 这个文件定义了一些变量,里面是Psr4的相关协议,后面类的自动加载的时候会使用到,等下会重点讲这个东西
require __DIR__ . '/autoload_static.php';
// 这段代码是执行一个回调函数,等下会让你看明白
call_user_func(\Composer\Autoload\ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
现在我们看看autoload_static.php
文件内容
class ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de
{
// 使用composer加载进来的类,你可以使用composer require topthink/think-captcha
// 引入验证码类,你会发现这里会多了一项内容,试试看!
public static $files = array (
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => __DIR__ . '/..' . '/topthink/think-orm/stubs/load_stubs.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
);
// 一时半会不知道怎么描述这个东西,只好直白一点了
// $prefixLengthsPsr4是个二维数组,think\\trace\\这是命名空间,作为键名,然后长度作为值,注意这里
// “\\”只能算一个字符,因为反斜杠是转义符,最外层是使用命名空间的第一个字符作为键名
public static $prefixLengthsPsr4 = array (
't' =>
array (
'think\\trace\\' => 12,
'think\\' => 6,
),
// 省略部分代码
);
// 这个变量定义的是命名空间对应的目录,就是对目录进行归类,后面自动加载类的时候,只有满足了这些对应关系的 // 类才能被加载,后面你将深有体会
public static $prefixDirsPsr4 = array (
'think\\trace\\' =>
array (
0 => __DIR__ . '/..' . '/topthink/think-trace/src',
),
'think\\' =>
array (
0 => __DIR__ . '/..' . '/topthink/framework/src/think',
1 => __DIR__ . '/..' . '/topthink/think-filesystem/src',
2 => __DIR__ . '/..' . '/topthink/think-helper/src',
3 => __DIR__ . '/..' . '/topthink/think-orm/src',
),
// 省略部分代码
);
// extend是不是很熟悉,自定义的类就是放在这个目录
public static $fallbackDirsPsr0 = array (
0 => __DIR__ . '/../..' . '/extend',
);
// 这个可以理解为缓存变量,后面也会用到
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
// 这是一个初始化函数,实现对象之间的变量复制,简单的说就是把一个类里面的成员变量的值赋给另一个类
public static function getInitializer(ClassLoader $loader)
{ // 这里返回的是一个Closure对象,Closure::bind后面很多地方都用到这个函数
// 大家可以看官方文档:https://www.php.net/manual/zh/closure.bind
return \Closure::bind(function () use ($loader) {
// 这里的$loader其实就是ClassLoader类,这个函数的功能就是将当前类的这些成员变量的值赋值给
// ClassLoader.php这个类里面的成员变量
$loader->prefixLengthsPsr4 = ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::$prefixDirsPsr4;
$loader->fallbackDirsPsr0 = ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::$fallbackDirsPsr0;
$loader->classMap = ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::$classMap;
}, null, ClassLoader::class);
}
}
我们再回来看看这行代码
// call_user_func函数的作用就是把第一个参数作为回调函数调用,也就是说把Closure对象作为一个函数调用,实现
// 对象与对象之间的变量复制,call_user_func具体用法可以看文档:
// https://www.php.net/manual/zh/function.call-user-func
call_user_func(\Composer\Autoload\ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::getInitializer($loader));
继续阅读源码
// 这个函数的核心代码
$loader->register(true);
// 下面的代码是引入composer加载进来的类,获取autoload_static.php里面的$files
$filesToLoad = \Composer\Autoload\ComposerStaticInit71a72e019c6be19dac67146f3f5fb8de::$files;
// 这里使用了内置函数Closure::bind定义了一个匿名函数,这个函数的作用其实就是引入相关类
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
// 判断全局变量是否有该类已经被引入的标识
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
// 存储一个标识,下次就不用重复引入
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
// 这里就是一个循环调用,引入相关类
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
这部分代码中,我们又接触到了Closure::bind,它绑定了一个静态的匿名函数,这函数里面的内容是这样的:先判断类是否被引入过,如果没有,则使用require引入,并且在全局变量中存储一个加载的标识。
简单的讲你可以把它看成一个函数,它就像你平时写的函数一样
function requireFile($fileIdentifier, $file){
……………………………………
}
其实很多人会有这样的一个疑问,为什么要使用Closure::bind
,而不是直接在foreach
里面写逻辑,另外写一个函数也行?答案就留给大家思考。
接下来就重点看看最核心的一个函数register(true)
public function register($prepend = false)
{
// 类的自动加载注册函数,一切逻辑都在loadClass这个函数里面
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
重点就在第一行代码,spl_autoload_register
装载了loadClass
这样一个函数
public function loadClass($class)
{
// 判断“被引入的类”文件是否存在
if ($file = $this->findFile($class)) {
// self::$includeFile当前类的成员变量,它是一个Closure对象,在初始化当前类里面就已经被定义了
/*
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
// 这里定义了这个Closure对象
self::initializeIncludeClosure();
}
*/
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
接下来我们看看$this->findFile($class)
public function findFile($class)
{
// 判断当前类的成员变量classMap是否存储了“被引入类”的路径,这个变量的初始化内容其实就 // 是 autoload_static.php的$classMap
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
// 判断“被引入类”是否存在,不存在直接返回false
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
// 这段代码其实就是从缓存中获取类的路径,目的就是提高框架的初始化速度,因为框架每次运行都要引入几十个类。
if (null !== $this->apcuPrefix) {
// 获取缓存内容,apcu_fetch函数大家可以看官方文档
// https://www.php.net/manual/zh/function.apcu-fetch
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
// 这个函数的核心代码
$file = $this->findFileWithExtension($class, '.php');
// 这段代码是跟黑客相关的,防止黑客入侵一些hh类型文件
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
// 这里就是把加载类路径缓存起来
if (null !== $this->apcuPrefix) {
// apcu_add跟apcu_fetch一样,去看看官方文档
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// 如果这个文件不存在,就存一个标识,下次就直接返回false即可
$this->missingClasses[$class] = true;
}
return $file;
}
下面我们来看看这个函数中最核心的一行代码
$file = $this->findFileWithExtension($class, '.php');
进入findFileWithExtension
// 我们以一个例子来讲,new think\Exception()这是框架载入的第一个类,此时传进来
// 的$class是think\Exception
private function findFileWithExtension($class, $ext)
{
// $logicalPathPsr4 = think\Exception.php
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
// 获取第一个字符"t",为什么?
$first = $class[0];
// 判断prefixLengthsPsr4这个数组中是否存在“t”这个元素,这里的prefixLengthsPsr4就是我们前面提到 // psr4协议规范的内容,你可以打开autoload_static.php看看,很显然是存在的
/*
't' =>
array (
'think\\trace\\' => 12,
'think\\captcha\\' => 14,
'think\\' => 6,
),
*/
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
// 这里的目的就是得到think\\这样的一个命名空间
$search = $subPath . '\\';
// 那接下来就是找该命名空间下面的目录
/*
'think\\' =>
array (
0 => __DIR__ . '/..' . '/topthink/framework/src/think',
1 => __DIR__ . '/..' . '/topthink/think-filesystem/src',
2 => __DIR__ . '/..' . '/topthink/think-helper/src',
3 => __DIR__ . '/..' . '/topthink/think-orm/src',
),
*/
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
// 遍历这四个目录,看看是否可以找到think\Exception.php
if (file_exists($file = $dir . $pathEnd)) {
// 最后返回F:\phpstudy_pro\WWW\thinkphp8\vendor
// \composer/../topthink/framework/src/think\Exception.php
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// 后面这部分代码是涉及到PSR-0,这里就不讲了,框架好像也并没有使用这种协议,但好像有个比较特别的地方
// PSR-0 fallback dirs
// 我们在autoload_static.php中看到$fallbackDirsPsr0这样一个变量而不是$fallbackDirsPsr4,
// 这样很让人费解,我也不知道是什么原因
// 这段代码其实就是定义了类的扩展目录,也就是说你自己的类放在extend这个目录里面会被框架自动加载
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
return false;
}
相信看到这里,大家对类的自动加载有了一定的认识。
记得我刚才出来那会,犯过这样的一个错误,就是把一个项目中通过composer引入的类,复制到另一个项目,发现运行不了,阅读源码之后才发现了真实的原因.