Composer 自动加载原理

未匹配的标注

简介

本篇主要剖析 Composer 如何做类的自动加载

剖析过程主要以 http://localhost/register 请求的生命周期为主线,暂不涉及 if 语句不会执行到的代码。

源代码阅读方式指南

本教程源代码剖析,能在代码注释中写明的,不会放到外面进行剖析,一些补充可能会放在代码后面进行单独说明。

因为代码执行涉及多个文件,且每个文件之间存在父与子的树形结构,故代码阅读方式,请遵从树的 前序遍历 原则,防止阅读迷茫。

代码执行顺序由 (1)、(2)、(3) ... (n) 方式标注

注释中特殊字符说明

[请到 <<某某小标题代表的文件>> 继续阅读] : 代表此行代码调用了另一个文件的方法,你应该去那个文件阅读,否则不明白是如何执行的。

Laravel 入口文件

public/index.php

<?php

// (1) 定义了一个 LARAVEL_START 的常量,并赋值了小数点化的微秒数,作用就是记录 Laravel 启动的时间点
define('LARAVEL_START', microtime(true));

// (2) 将导入 vendor/autoload.php 文件,并执行此文件,将此文件中的变量、类、函数注入 PHP 运行内存中 [请到 <<Composer 自动加载的入口文件>> 继续阅读]
require __DIR__.'/../vendor/autoload.php';
// (3) Composer 自动加载原理完结

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);
  • 额外注意

    在 PhpStrom 步调工具中,如果点击步入函数按钮,将进入 vendor/autoload.php 中进行步调;如果点击跳过函数按钮,将在当前文件跳到下一行,并把 vendor/autoload.php 中的变量、类、函数直接注入内存中,无论 vendor/autoload.php 内部调用多少类、多少函数,直接全部执行完毕。

  • 知识扩展

    __DIR__:返回当前文件所在绝对路径,记住是当前文件哦;不是 index.php的绝对路径,是有 __DIR__ 代码的文件所在绝对路径哦。

Composer 自动加载的入口文件

请看完 <<Laravel 入口文件>> 第 (2) 步,再看此文件

vendor/autoload.php

<?php

// (1) 真 • Composer 入口文件
require_once __DIR__ . '/composer/autoload_real.php';

// (2) 返回自动加载对象,我们可以用它做动态类名绑定哦 [请到 <<真 • Composer 自动加载入口文件>> 继续阅读]
return ComposerAutoloaderInite12a16f2d7bcbc7a72b9f10faab9dc8b::getLoader();

真 • Composer 自动加载入口文件

请看完 <<Composer 自动加载的入口文件>> 的第 (2) 步,再看此文件

vendor/composer/autoload_real.php

<?php

class ComposerAutoloaderInite12a16f2d7bcbc7a72b9f10faab9dc8b
{
    /**
     * 静态对象类型变量,类似单例模式的只有一个对象,防止二次实例化
     */
    private static $loader;

    /**
     * 先有鸡后有蛋,想自动加载其它类,先自动加载自己吧
     */
    public static function loadClassLoader($class)
    {
        // (4) $class 形参就是代表 PHP 没有载入的类全名(包含命名空间,且开头的 \ 去掉)
        if ('Composer\Autoload\ClassLoader' === $class) {
            // (5) 既然没有载入,那么就载入吧
            require __DIR__ . '/ClassLoader.php';
        }
    }

    /**
     * 自动加载主方法,等于 C 中 main 函数,反正是主要的就是啦
     */
    public static function getLoader()
    {
        // (1) 类似单例模式的操作
        if (null !== self::$loader) {
            return self::$loader;
        }

        // (2) 将 loadClassLoader 静态方法添加至 PHP 自动加载队列
        spl_autoload_register(array('ComposerAutoloaderInite12a16f2d7bcbc7a72b9f10faab9dc8b', 'loadClassLoader'), true, true);

        // (3) PHP没有载入 Composer\Autoload\ClassLoader 类,去找 PHP 自动加载队列,并执行里面的 loadClassLoader,然后载入 Composer\Autoload\ClassLoader 类,成功 new 一个对象。
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();

        // (6) 将 loadClassLoader 静态方法从 PHP 自动加载队列中删除
        spl_autoload_unregister(array('ComposerAutoloaderInite12a16f2d7bcbc7a72b9f10faab9dc8b', 'loadClassLoader'));

        /*
         * (7)
         *
         * 第一个表达式 PHP_VERSION_ID >= 50600: 由于我用的是 PHP7.2 故,此表达式是 true,与运算未短路,执行下一表达式
         * 第二个表达式 !defined('HHVM_VERSION'): 没有用 HHVM 虚拟机则返回 true,我的确没用,未短路,继续下一个表达式
         * 第三个表达式 (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()) 分解如下: 
         * 第三个表达式中第一个表达式 !function_exists('zend_loader_file_encoded'): 如果不存在 zend_loader_file_encoded 函数返回 true。经过测试我 PHP 中的确没有 zend_loader_file_encoded 函数,故与第三表达式中第二表达式短路。
         * 整体返回 true
         */
        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

        if ($useStaticLoader) {

            // (8) 加载 vendor\composer\autoload_static.php 静态的自动加载映射类
            require_once __DIR__ . '/autoload_static.php';

            // (9) 调用静态自动加载映射类的 getInitializer 方法,此方法将返回一个闭包,再通过执行此闭包,将自动加载映射规则注入到 loader 对象中 [请到 <<Composer 自动加载映射类>> 继续阅读]
            call_user_func(\Composer\Autoload\ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b::getInitializer($loader));
        } else {
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            $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);
            }
        }

        // (10) 调用 loader 对象的 register 方法,把 loadClass 方法添加到 PHP 自动加载队列中 [请到 <<Composer 核心自动加载类>> 继续阅读]
        $loader->register(true);

        if ($useStaticLoader) {

            // (11) 返回 Laravel 全局函数文件的绝对路径,准备加载全局函数
            $includeFiles = Composer\Autoload\ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }

        foreach ($includeFiles as $fileIdentifier => $file) {

            // (12) 取全局函数 文件标识码 和 文件绝对路径 执行循环加载
            composerRequiree12a16f2d7bcbc7a72b9f10faab9dc8b($fileIdentifier, $file);
        }

        return $loader;
    }
}

function composerRequiree12a16f2d7bcbc7a72b9f10faab9dc8b($fileIdentifier, $file)
{
    // (13) 根据全局函数 文件标识码 查看是否加载过,防止二次加载
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {

        // (14) 执行全局函数加载
        require $file;

        // (15) 转换全局函数加载状态,防止二次加载
        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

Composer 自动加载映射类

请看完 <<真 • Composer 自动加载入口文件>> 的第 (9) 步,再看此文件

vendor\composer\autoload_static.php

<?php

namespace Composer\Autoload;

class ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b
{
    /**
     * 这些就是待加载的 Laravel 全局函数
     */
    public static $files = array (
        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',

        // ... 还有更多,结构类似
    );

    /**
     * Psr4 键的长度;\\ 代表转义 \,实际长度为 1;算是一种索引优化,能够迅速定位 Psr4 加载规则所对应的键值对
     */
    public static $prefixLengthsPsr4 = array (
        'p' => 
        array (
            'phpDocumentor\\Reflection\\' => 25,
        ),
        'X' => 
        array (
            'XdgBaseDir\\' => 11,
        ),

        // ... 还有更多,结构类似
    );

    /**
     * Psr4 映射对应规则:数组键 是类所定义的命名空间的公共前缀,数组值 是所对应的实际文件的相对地址空间
     */
    public static $prefixDirsPsr4 = array (
        'phpDocumentor\\Reflection\\' => 
        array (
            0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
            1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
            2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
        ),

        // ... 还有更多,结构类似
    );

    /**
     * Psr0 映射对应规则:与 Psr4 不同是数组结构不一样,但实现功能都是殊途同归的
     */
    public static $prefixesPsr0 = array (
        'P' => 
        array (
            'Prophecy\\' => 
            array (
                0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
            ),
            'Parsedown' => 
            array (
                0 => __DIR__ . '/..' . '/erusev/parsedown',
            ),
        ),

        // ...还有更多,结构类似
    );

    /**
     * classMap 呵呵,类全名 对应 类所在绝对路径,简单的一比
     */
    public static $classMap = array (
        'App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php',
        'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',

        // ... 还有非常多,结构类似
    );

    public static function getInitializer(ClassLoader $loader)
    {
        // (1) 返回一个闭包,此闭包将在 <<真 • Composer 自动加载入口文件>> 第 (9) 步中通过 call_user_func 函数被调用。
        return \Closure::bind(function () use ($loader) {

            // (2) 下面这四行,代表将本文件中的 Psr4、Psr0 和 classMap 属性赋值到 loader 对象中。
            $loader->prefixLengthsPsr4 = ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b::$prefixDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b::$prefixesPsr0;
            $loader->classMap = ComposerStaticInite12a16f2d7bcbc7a72b9f10faab9dc8b::$classMap;

        }, null, ClassLoader::class);
    }
}
  • 关于闭包函数(闭包对象)

    PHP 里面实现像 JavaScript 中的闭包,是通过一个内置对象实现的,这个对象是 Closure

    凡是对象都有属性和方法,同样 Closure 对象也有属性和方法。你肯定会问,既然是对象,怎么能通过 Object() 或者 call_user_func('Object') 来执行呢。那是因为 Closure 有一个魔术方法 __invoke()。关于此方法看这 __invoke()魔术方法

  • 关于闭包函数中的 use

    function () use ($loader) {}

    因为闭包函数内与外是不同的空间,因此内部无法直接使用外部的变量的。只能通过 use 将外部的变量存放到 Closure 对象的 static 属性中,是以数组的形式存放的哦。这样在执行 __invoke() 时,就调用 static 中的键值对就可以了

  • 注:$this 无需使用 use 引用可直接在 Closure 对象中使用。这个我理解的是,可能在类中声明的闭包对象默认继承自当前类,不知道对不

  • 关于闭包函数的参数

    会存放在闭包对象的 parameter 属性中,也是以数组的形式存放

  • 一个闭包对象的数据详情

    file

  • 关于使用 return \Closure::bind() 方法,而不直接使用 return function() 一些想法

    你可能注意到,上面声明闭包的方式与传统的不一样。那是因为 ClassLoader 类里面 $classMap 等属性是私有属性,只用通过 Closure::bind 方法重新定义闭包所属类,才能在闭包中调用其它类的私有属性。

Composer 核心自动加载类

请看完 <<真 • Composer 自动加载入口文件>> 的第 (10) 步,再看此文件

vendor\composer\ClassLoader.php

<?php

namespace Composer\Autoload;

/**
 * 注:类里面的方法和属性顺序做了调整,并删除掉一些很少用的属性和方法,方便阅读
 */
class ClassLoader
{

    /**
     * 下面四个是前面被赋值的静态映射数组
     */
    private $prefixLengthsPsr4 = array();
    private $prefixDirsPsr4 = array();
    private $prefixesPsr0 = array();
    private $classMap = array();

    /**
     * 获取相应的数组映射
     */
    public function getClassMap()
    {
        return $this->classMap;
    }
    public function getPrefixesPsr4(){}
    public function getPrefixes(){}

    /**
     * 添加 ClassMap 额外的映射
     */
    public function addClassMap(array $classMap)
    {
        if ($this->classMap) {
            $this->classMap = array_merge($this->classMap, $classMap);
        } else {
            $this->classMap = $classMap;
        }
    }

    /**
     * 添加 Psr0 额外的映射
     */
    public function add($prefix, $paths, $prepend = false){}

    /**
     * 添加 Psr4 额外的映射
     */
    public function addPsr4($prefix, $paths, $prepend = false){}

    /**
     * 核心方法:将 Laravel 自动加载运行的方法 loadClass 添加至 PHP 自动加载序列
     */
    public function register($prepend = false)
    {
        // (1) 从 <<真 • Composer 自动加载入口文件>> 第 (10) 步过来的
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
    }

    /**
     * 将 Laravel 自动加载运行的方法从 PHP 自动加载序列中删除。
     */
    public function unregister()
    {
        spl_autoload_unregister(array($this, 'loadClass'));
    }

    /**
     * !!!核心中的核心:以后的代码运行,碰到没有加载的类,就会来这里加载!!!
     */
    public function loadClass($class)
    {
        if ($file = $this->findFile($class)) {
            includeFile($file);

            return true;
        }
    }

    /**
     * 被核心中的核心调用了的方法,主要从 类 与 路径 映射中取出 类 所在的文件绝对路径,方便接下来的类加载
     */
    public function findFile($class)
    {
        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class];
        }

        // ...Psr0 和 Psr4 加载逻辑代码,这里暂且不看
    }
}

/**
 * 阻止包含文件访问 $this 或 self :作用就是防止在待加载的 php 文件中使用 $this 或 self,从而直接访问了本 <<Composer 核心自动加载类>>,从而破坏因果关系,导致未知错误发生。说白了就是我的就是我的,不是我的,我也不能拿
 */
function includeFile($file)
{
    include $file;
}

全局函数文件简要说明

public static $files = array (
        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
        '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
        '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
        '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
        '2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php',
        'f0906e6318348a765ffb6eb24e0d0938' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
        '58571171fd5812e6e447dce228f52f4d' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/helpers.php',
        '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
        '801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php',
        '4e8671d7be9056dcd04ddd9e8e15f9cc' => __DIR__ . '/..' . '/encore/laravel-admin/src/helpers.php',
    );

vendor/symfony/polyfill-mbstring/bootstrap.php: Symfony 组件根据 PHP 的 mb_string 扩展定义了一些字符串处理的函数

vendor/symfony/polyfill-php72/bootstrap.php: 如果 PHP 版本小于7.2,则此文件会定义7.2版本新特性函数供低版本的 PHP 使用

vendor/symfony/var-dumper/Resources/functions/dump.php: 传说中 dump 和 dd 函数就在这里面哦

vendor/symfony/polyfill-ctype/bootstrap.php: 如果 PHP 内置没有ctype 系列函数, 则定义的这些函数

vendor/swiftmailer/swiftmailer/lib/swift_required.php: 注册了 Swift 相关类的自动加载函数

vendor/laravel/framework/src/Illuminate/Foundation/helpers.php: Laravel 的相关辅助函数

vendor/laravel/framework/src/Illuminate/Support/helpers.php: 也是 Laravel 的相关辅助函数

vendor/myclabs/deep-copy/src/DeepCopy/deep_copy.php: 定义一个深度拷贝功能的一个函数,执行此函数返回是深度拷贝的相关服务对象

vendor/psy/psysh/src/functions.php: psy 相关的一些辅助函数

总结

  • Composer 自动加载的入口文件 vendor\autoload.php

    所有使用 Composer 自动加载功能大门。

  • 真 • Composer 自动加载入口文件 vendor\composer\autoload_real.php

    Composer 内部执行的入口文件,这个文件相当于加工者。

    它将 自动加载映射类核心自动加载类 保存在自己身体里,通过逻辑操作,将 自动加载映射类 定义的 类全名 => 类绝对路径 赋值到 核心自动加载类 中。

    然后执行一项重要操作:调用 核心自动加载类 中的 register 函数,将 loadClass 方法添加 PHP 自动加载序列中,以供整个 Laravel 类的自动加载需求。

    再然后通过 自动加载映射类 定义的全局函数路径,加载 Laravel 所需全局函数。

    最后返回 核心自动加载类

  • Composer 自动加载映射类 vendor\composer\autoload_static.php

    它主要定义了 全局函数标识码 => 函数文件所在路径 以及 类全名 => 类绝对路径 的有关映射,类型为数组。

    同时还定义了一个闭包,用来将这些映射赋值到 核心自动加载类

  • Composer 核心自动加载类 vendor\composer\ClassLoader.php

    实现 类自动加载方法注册 和 根据类名查询类所在绝对路径 的功能

    实现整个 Laravel 类加载时所需的自动加载方法

本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3
发起讨论 查看所有版本


郎中航
Closure::bind 这个方法不是很懂
0 个点赞 | 4 个回复 | 分享 | 课程版本 5.6
tomtiddler
闭包绑定类、做个笔记
0 个点赞 | 0 个回复 | 分享 | 课程版本 5.6