Laravel 中 composer 加载流程

2023-08-23 18:35:18 浏览数 (1)

启动

  • Laravel 5.8

文章以 Laravel 学习。入口文件 public/index.php

代码语言:javascript复制
// Register The Auto Loader
require __DIR__.'/../vendor/autoload.php';

autoload.php 不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。

autoload_real.php 中的类名为 ComposerAutoloaderInit... 这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。

其实还有一个做法我们更加熟悉,是定义一个命名空间。这里为什么不定义一个命名空间呢?一种理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。

autoload_real.php

autoload.php 主要调用了 getLoader()

代码语言:javascript复制
public static function getLoader()
{
    // 单例模式,自动加载类只能有一个 1
    if (null !== self::$loader) {
        return self::$loader;
    }

    // 获得自动加载核心类对象 2
    spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
    self::$loader = $loader = new ComposerAutoloadClassLoader();
    spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));

    // 初始化自动加载核心类对象 3
    $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
    if ($useStaticLoader) {
        require_once __DIR__ . '/autoload_static.php';

        call_user_func(ComposerAutoloadComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::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);
        }
    }

    // 注册自动加载核心类对象 4
    $loader->register(true);

    // 自动加载全局函数 5
    if ($useStaticLoader) {
        $includeFiles = ComposerAutoloadComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);
    }

    return $loader;
}

单例模式 1

代码语言:javascript复制
if (null !== self::$loader) {
    return self::$loader;
}

构造 ClassLoader 核心类 2

代码语言:javascript复制
spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
self::$loader = $loader = new ComposerAutoloadClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));
代码语言:javascript复制
public static function loadClassLoader($class)
{
    if ('ComposerAutoloadClassLoader' === $class) {
        require __DIR__ . '/ClassLoader.php';
    }
}

composer 先向 PHP 自动加载机制注册了一个函数,这个函数 requireClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。

为什么不直接 require?原因是:怕有的用户也定义了个 ComposerAutoloadClassLoader 命名空间,导致自动加载错误文件。

那为什么不跟引导类一样用个哈希值呢?原因是:这个类是可以复用的,框架允许用户使用这个类。

初始化核心类对象 3

对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。初始化的方法有两种:

  1. 使用 autoload_static 进行静态初始化
  2. 调用核心类接口初始化

autoload_static 静态初始化

静态初始化只支持 PHP 5.6 以上版本、不支持 HHVM 虚拟机、不存在 Zend-encoded file

autoload_static.php

代码语言:javascript复制
<?php

// autoload_static.php @generated by Composer

namespace ComposerAutoload;

// hash 防止冲突
class ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db
{
    public static $files = array (...);
    public static $prefixLengthsPsr4 = array (...);
    public static $prefixDirsPsr4 = array (...);
    public static $fallbackDirsPsr4 = array (...);
    public static $prefixesPsr0 = array (...);
    public static $classMap = array array (...);

    public static function getInitializer(ClassLoader $loader)
    {
        return Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixDirsPsr4;
            $loader->fallbackDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$fallbackDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixesPsr0;
            $loader->classMap = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$classMap;

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

这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。

值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader 中的 prefixLengthsPsr4prefixDirsPsr4 等等方法都是 private 的。普通的函数没办法给类的 private 成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 ClassLoader 类的成员函数。

关于匿名函数的 绑定功能。

接下来就是 顶级命名空间 初始化的关键了。

classMap
代码语言:javascript复制
public static $classMap = array (
    'App\Api\Middleware\DeviceRecord' => __DIR__ . '/../..' . '/app/Api/Middleware/DeviceRecord.php',
    'App\Api\Middleware\HeaderCheck' => __DIR__ . '/../..' . '/app/Api/Middleware/HeaderCheck.php',
    ...
)

直接命名空间全名与目录的映射,没有顶级命名空间。简单粗暴,也导致这个数组相当的大。

PSR0 顶级命名空间映射
代码语言:javascript复制
public static $prefixesPsr0 = array (
    'P' =>
    array (
        'Prophecy\' =>
        array (
            0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
        ),
        'Parsedown' =>
        array (
            0 => __DIR__ . '/..' . '/erusev/parsedown',
        ),
    ),
    ...
);

为了快速找到顶级命名空间,这里使用命名空间第一个字母作为前缀索引。这个映射的用法比较明显,假如我们有 Parsedown/example 这样的命名空间,首先通过首字母 P,找到:

代码语言:javascript复制
'P' => array (...)

这个数组,然后就会遍历这个数组来和 Parsedown/example 比较,发现第一个 Prophecy 不符合,第二个 Parsedown 符合,然后得到了映射目录(映射目录可能不止一个):

代码语言:javascript复制
0 => __DIR__ . '/..' . '/erusev/parsedown',

接着遍历这个数组,尝试 __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php' 是否存在,如果不存在接着遍历数组(这个例子数组只有一个元素),如果数组遍历完都没有,就会加载失败。

PSR4 标准顶级命名空间映射
代码语言:javascript复制
public static $prefixLengthsPsr4 = array (
    'p' =>
    array (
        'phpDocumentor\Reflection\' => 25,
    ),
    'Z' =>
    array (
        'Zend\Diactoros\' => 15,
    ),
    ...
);

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',
    ),
    'Zend\Diactoros\' =>
    array (
        0 => __DIR__ . '/..' . '/zendframework/zend-diactoros/src',
    ),
    ...
);

PSR4 标准 顶级命名空间 映射用了两个数组,第一个和 PSR0 一样用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是 顶级命名空间 的长度。为什么呢?因为 PSR4 的文件目录更加灵活,更加简洁。

PSR0顶级命名空间 目录 直接加 到命名空间前面就可以得到路径:

代码语言:javascript复制
                                        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php
                                                         ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

PSR4 却是用顶级命名空间目录 替换 顶级命名空间,所以获得顶级命名空间的 长度 很重要:

代码语言:javascript复制
                                        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php
                                                ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

举例:假如我们找 Symfony\Polyfill\Mbstring\example 这个类,和 PSR0 一样通过前缀索引和字符串匹配我们得到了:

代码语言:javascript复制
'Symfony\Polyfill\Mbstring\' => 26,

这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4 获取它的映射目录数组(注意映射目录可能不止一条):

代码语言:javascript复制
'Symfony\Polyfill\Mbstring\' =>
array (
    0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),

Symfony\Polyfill\Mbstring\example 前 26 个字母替换为 __DIR__ . '/..' . '/symfony/polyfill-mbstring 也就是:

代码语言:javascript复制
__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php

先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。

自动加载核心类 ClassLoader 的静态初始化完成!

其实还有 $fallbackDirsPsr4,暂未研究

调用接口初始化

如果 PHP 版本低于 5.6 或者使用 HHVM 虚拟机环境或者存在 zend_loader_file_encoded,那么就要使用核心类的接口进行初始化。

代码语言:javascript复制

/*
PSR0 取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面的 autoload_static 的 $prefixesPsr0。

如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。
*/
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}

/*
PSR4 如果没有顶级命名空间,就直接保存目录。
如果有命名空间的话,要保证顶级命名空间最后是 ,然后分别保存
( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )
( 顶级命名空间 -> 目录 )

这两个映射数组。具体形式可以查看上面我们讲的 autoload_static 的 prefixLengthsPsr4、$prefixDirsPsr4 。
*/
$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);
}

注册核心类对象 4

Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,换句话说,如果有命名空间 AppConsoleKernel,我们已经知道了 App 对应的目录,接下来我们就要解决下面的就是 ConsoleKernel 这一段。

代码语言:javascript复制
/**
 * Registers this instance as an autoloader.
 *
 * @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

一行代码实现自动加载。核心在 ClassLoaderloadClass() 函数上,这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 AppConsoleKernelConsoleKernel 这一段转为目录。

自动加载全局函数 5

Composer 不止可以自动加载命名空间,还可以加载全局函数。就是把全局函数写到特定的文件里面去,在程序运行前挨个 require 就行了。

代码语言:javascript复制
if ($useStaticLoader) {
    // 静态初始化
    $includeFiles = ComposerAutoloadComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;
} else {
    // 普通初始化
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);
}
代码语言:javascript复制
function composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

问题 1

为什么不直接 require $includeFiles 里面的每个文件名,而要用类外面的函数 composerRequire...

  • 避免和用户定义函数冲突
  • 防止有人在全局函数所在的文件写 $this 或者 self

假如 includeFiles 有个 app/helper.php 文件,这个 helper.php 文件的函数外有一行代码: this->foo(),如果引导类在 getLoader() 函数直接 require(

事实上 helper.php 就不应该出现 $thisself 这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生:

  • 第一种情况:引导类恰好有 foo() 函数,那么就会莫名其妙执行了引导类的 foo()
  • 第二种情况:引导类没有 foo() 函数,但是却甩出来引导类没有 foo() 方法这样的错误提示,用户不知道自己哪里错了。把 require 语句放到 引导类的外面,遇到 this 或者 self ,程序就会告诉用户根本没有类, this 或 self 无效,错误信息更加明朗。

问题 2

为什么要用 hash 作为 $fileIdentifier

这个变量是用来控制全局函数只被 require 一次的,那为什么不用 require_once 呢?事实上 require_oncerequire 效率低很多,使用全局变量 $GLOBALS 这样控制加载会更快。猜测另一个原因应该是 require_once 对相对路径的支持并不理想,所以 composer 尽量少用 require_once

运行

ClassLoaderloadClass() 函数注册到 PHP SPL 中的 spl_autoload_register() 里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register() 里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。

代码语言:javascript复制
/**
 * Loads the given class or interface.
 *
 * @param  string    $class The name of the class
    * @return bool|null True if loaded, null otherwise
    */
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file); // include $file; Prevents access to $this/self from included files.

        return true;
    }
}

/**
 * Finds the path to the file where the class is defined.
 *
 * @param string $class The name of the class
    *
    * @return string|false The path if found, false otherwise
    */
public function findFile($class)
{
    // class map lookup
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }

    // classMapAuthoritative 关闭搜索未在类映射中注册的类的 prefix and fallback directories。- 不清楚干啥的 暂没研究
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }

    // 如果启用扩展名,则使用 APCu 前缀来缓存已找到/未找到的类。 - 不清楚干啥的 暂没研究
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }

    $file = $this->findFileWithExtension($class, '.php');

    // Search for Hack files if we are running on HHVM
    if (false === $file && defined('HHVM_VERSION')) {
        $file = $this->findFileWithExtension($class, '.hh');
    }

    if (null !== $this->apcuPrefix) {
        apcu_add($this->apcuPrefix.$class, $file);
    }

    if (false === $file) {
        // Remember that this class does not exist.
        $this->missingClasses[$class] = true;
    }

    return $file;
}

loadClass() 主要调用 findFile() 函数。findFile() 在解析命名空间的时候主要分为两部分:

  • classMap 直接看命名空间是否在映射数组
  • findFileWithExtension() 包含了 PSR0PSR4

如果我们在代码中写 'phpDocumentorReflectionexample,PHP 会通过 SPL 调用 loadClass -> findFile -> findFileWithExtension

  • 首先默认用 .php 后缀名调用 findFileWithExtension 函数里,利用 PSR4 标准尝试解析目录文件,如果文件不存在则继续用 PSR0 标准解析
  • 如果解析出来的目录文件仍然不存在,但是环境是 HHVM 虚拟机,继续用后缀名 .hh 再次调用 findFileWithExtension 函数,如果不存在,说明此命名空间无法加载,放到 classMap 中设为 false,以便以后更快地加载

PSR4

对于 phpDocumentorReflectionexample,当尝试利用 PSR4 标准映射目录时,步骤如下:

代码语言:javascript复制
// $class: phpDocumentorReflectionexample

// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\', DIRECTORY_SEPARATOR) . $ext;
// $logicalPathPsr4: phpDocumentor/Reflection/example.php(hh)`

$first = $class[0];
// $first: p

if (isset($this->prefixLengthsPsr4[$first])) {
    /* 'p' =>
    array (
        'phpDocumentor\Reflection\' => 25,
    ),
    */
    $subPath = $class;
    // $subPath: phpDocumentorReflectionexample
    while (false !== $lastPos = strrpos($subPath, '\')) {
        // $lastPos 13
        $subPath = substr($subPath, 0, $lastPos);
        $search = $subPath.'\';
        if (isset($this->prefixDirsPsr4[$search])) {
            // search phpDocumentor\Reflection\
            // $lastPos 25

            /* 'phpDocumentor\Reflection\' =>
                array (
                    0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
                    1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
                    2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
                ),
            */
            $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos   1);
            // $pathEnd /example.php(hh)

            foreach ($this->prefixDirsPsr4[$search] as $dir) {
                // 遍历 3 个
                if (file_exists($file = $dir . $pathEnd)) {
                    // $file __DIR__ . '/..' . /phpdocumentor/type-resolver/src/example.php(hh)`
                    return $file;
                }
            }
        }
    }
}

PSR0

如果 PSR4 标准加载失败,则要进行 PSR0 标准加载。对于 phpDocumentorReflectionexample,当尝试利用 PSR0 标准映射目录时,步骤如下:

代码语言:javascript复制
// $class: phpDocumentorReflectionexample

// PSR-0 lookup
if (false !== $pos = strrpos($class, '\')) {
    // namespaced class name
    $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos   1)
        . strtr(substr($logicalPathPsr4, $pos   1), '_', DIRECTORY_SEPARATOR);
} else {
    // PEAR-like class name
    $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}

// $logicalPathPsr0: phpDocumentor/Reflection/example.php(hh)`
if (isset($this->prefixesPsr0[$first])) {
    foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
        /* 'P' =>
        array (
            'Prophecy\' =>
            array (
                0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
            ),
            'Parsedown' =>
            array (
                0 => __DIR__ . '/..' . '/erusev/parsedown',
            ),
        ), */
        if (0 === strpos($class, $prefix)) {
            foreach ($dirs as $dir) {
                if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                    // $file __DIR__ . '/..' . '/phpspec/prophecy/src' . phpDocumentor/Reflection/example.php(hh)
                    return $file;
                }
            }
        }
    }
}

Q&A

个人一些疑问:

防止用户自定义与 ClassLoader 命名空间冲突

代码语言:javascript复制
spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
self::$loader = $loader = new ComposerAutoloadClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));

为什么这样可以解决:与用户也定义了个 ComposerAutoloadClassLoader 命名空间,导致自动加载错误文件。

与第四个参数 $prepend true 有关吗?

composer StaticLoader 有什么优势

composer 在加载类和加载全局方法时,都有两种方式。

代码语言:javascript复制
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

$useStaticLoader 的值进行选择,为什么一定分两种,静态方法是有什么优势吗?

References

  • PHP Composer - 初始化源码分析

– EOF –

  • # php
  • # composer

0 人点赞