深度分析thinkphp类的自动加载

类的自动加载是框架中非常重要的特性,它允许你在使用类时无需手动包含或引入对应的文件 。类的自动加载实现起来很简单,只需这样的一个函数spl_autoload_register就能实现。但框架都有各自的加载规范,并不是所有类都能被自动加载,因此这节内容大家还可以了解到PSR-4的自动加载规范,另外也可以弄明白通过composer引入进来的类是如何被加载的。

带着我们的好奇心开始我们thinkphp源码之旅,打开入口文件public/index.php

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目录下可以找到该文件

php 复制代码
if (PHP_VERSION_ID < 50600) {
    /**版本相关的限制,省略代码**/
}
// 引入autoload_real.php,这个类是由composer自动生产的
require_once __DIR__ . '/composer/autoload_real.php';

// 调用该类里面的getLoader方法,这个类名有点长,这也是composer自动生成的
return ComposerAutoloaderInit71a72e019c6be19dac67146f3f5fb8de::getLoader();

接下来看看getLoader()方法做了什么

php 复制代码
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文件内容

ini 复制代码
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);
    }
}

我们再回来看看这行代码

ruby 复制代码
// 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));

继续阅读源码

php 复制代码
// 这个函数的核心代码
$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引入,并且在全局变量中存储一个加载的标识。

简单的讲你可以把它看成一个函数,它就像你平时写的函数一样

php 复制代码
function requireFile($fileIdentifier, $file){
    ..........................................
}   

其实很多人会有这样的一个疑问,为什么要使用Closure::bind,而不是直接在foreach里面写逻辑,另外写一个函数也行?答案就留给大家思考。

接下来就重点看看最核心的一个函数register(true)

php 复制代码
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这样一个函数

php 复制代码
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)

php 复制代码
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;
}

下面我们来看看这个函数中最核心的一行代码

ini 复制代码
$file = $this->findFileWithExtension($class, '.php');

进入findFileWithExtension

bash 复制代码
// 我们以一个例子来讲,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引入的类,复制到另一个项目,发现运行不了,阅读源码之后才发现了真实的原因.

相关推荐
张小勇2 天前
thinkphp通过with查询,并通过关联表进行筛选
thinkphp·fastadmin
一一程序6 天前
ThinkPHP-导入Excel表格(通用版)
php·excel·thinkphp·thinkphp导入excel
张小勇12 天前
thinkphp单独为某个接口设置缓存
thinkphp
洪、20 天前
记录一下PHP使用微信小程序支付
微信小程序·php·thinkphp
Z3r4y23 天前
【代码审计】star7th/showdoc:v3.2.4 Phar反序列化写webshell
web·代码审计·thinkphp·漏洞复现·showdoc·xve-2023-28617·guzzlehttp
521源码1 个月前
521源码-网站源码-Thinkphp聊天室H5实时聊天室群聊聊天室自动分配账户完群组/私聊/禁言等功能/全开源运营版本
php·thinkphp·对话·客服·聊天·群聊
poem-rain1 个月前
thinkphp6 queue队列的maxTries自定义
数据库·sql·php·thinkphp
图图爱上壮壮妈1 个月前
读取器 thinkphp 文件预处理
php·thinkphp·读取器
Yehger1 个月前
Thinkphp 使用Model来增删改查
运维·服务器·thinkphp
SuperherRo1 个月前
代码审计-PHP框架开发篇&ThinkPHP&版本缺陷&不安全写法&路由访问&利用链
php·thinkphp