解析 Laravel 的自动加载机制
laravel的自动加载机制
在laravel的请求生命周期中,Laravel应用的所有请求入口都是public/index.php
文件。
查看index.php代码如下,通过引入vendor/autoload.php类,实现laravel的自动加载机制。
<?php
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
查看vendor/autoload.php文件内容,如下:
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInita81fe60a8dd2eeedd232b605e91881ac::getLoader();
这里为laravel使用composer实现自动加载的开始
vendor/composer为实现自动加载的源文件,以下为源文件的内容:
- autoload_real.php: 自动加载功能的引导类。
- composer 加载类的初始化
(顶级命名空间与文件路径映射初始化)
和注册(spl_autoload_register())。
- composer 加载类的初始化
- ClassLoader.php : composer 加载类。
- composer 自动加载功能的核心类。
- autoload_static.php : 顶级命名空间初始化类,
- 用于给核心类初始化顶级命名空间。
- autoload_classmap.php : 自动加载的最简单形式,
- 有完整的命名空间和文件目录的映射;
- autoload_files.php : 用于加载全局函数的文件,
- 存放各个全局函数所在的文件路径名;
- autoload_namespaces.php : 符合 PSR0 标准的自动加载文件,
- 存放着顶级命名空间与文件的映射;
- autoload_psr4.php : 符合 PSR4 标准的自动加载文件,
- 存放着顶级命名空间与文件的映射;
查看autoload_real.php文件,文件内容如下:
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInita81fe60a8dd2eeedd232b605e91881ac
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInita81fe60a8dd2eeedd232b605e91881ac', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInita81fe60a8dd2eeedd232b605e91881ac', 'loadClassLoader'));
$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(\Composer\Autoload\ComposerStaticInita81fe60a8dd2eeedd232b605e91881ac::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);
}
}
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInita81fe60a8dd2eeedd232b605e91881ac::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequirea81fe60a8dd2eeedd232b605e91881ac($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequirea81fe60a8dd2eeedd232b605e91881ac($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
其中的getLoader()为主要实现自动加载的核心方法。
以下开始分析getLoader()方法的实现。
第一部分:实现自动加载单例,自动加载类只能有一个
if (null !== self::$loader) {
return self::$loader;
}
第二部分:构造ClassLoader()核心类
spl_autoload_register(array('ComposerAutoloaderInita81fe60a8dd2eeedd232b605e91881ac', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInita81fe60a8dd2eeedd232b605e91881ac', 'loadClassLoader'));
通过spl_autoload_register注册loadClassLoader做为__autoload 的实现并成功 new 出该文件中核心类 ClassLoader() 后,通过spl_autoload_unregister注销已注册的 __autoload()函数
第三部分:初始化核心类对象
$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(\Composer\Autoload\ComposerStaticInita81fe60a8dd2eeedd232b605e91881ac::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);
}
}
首先,初始化共有2种方法
第一种判断PHP的版本若大于5.6并且不支持 HHVM 虚拟机
使用call_user_func调用getInitializer()方法
深入了解autoload_static.php文件代码:
<?php
class ComposerStaticInit7b790917ce8899df9af8ed53631a1c29{
public static $files = array(...);
public static $prefixLengthsPsr4 = array(...);
public static $prefixDirsPsr4 = array(...);
public static $prefixesPsr0 = array(...);
public static $classMap = array (...);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4
= ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixLengthsPsr4;
$loader->prefixDirsPsr4
= ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixDirsPsr4;
$loader->prefixesPsr0
= ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixesPsr0;
$loader->classMap
= ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$classMap;
}, null, ClassLoader::class);
}
这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader类 中的 prefixLengthsPsr4 、prefixDirsPsr4等等变量都是 private的。利用匿名函数的绑定功能就可以将这些 private 变量赋给 ClassLoader类里的成员变量。
classMap(命名空间映射)
<?php
public static $classMap = array (
'App\\Console\\Kernel'
=> __DIR__ . '/../..' . '/app/Console/Kernel.php',
'App\\Exceptions\\Handler'
=> __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
'App\\Http\\Controllers\\Auth\\ForgotPasswordController'
=> __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php',
'App\\Http\\Controllers\\Auth\\LoginController'
=> __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php',
'App\\Http\\Controllers\\Auth\\RegisterController'
=> __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',
...)
直接命名空间全名与目录的映射,简单粗暴,也导致这个数组相当的大。
PSR4 标准顶级命名空间映射数组:
<?php
public static $prefixLengthsPsr4 = array(
'p' => array (
'phpDocumentor\\Reflection\\' => 25,
),
'S' => array (
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Component\\Yaml\\' => 23,
'Symfony\\Component\\VarDumper\\' => 28,
...
),
...);
public static $prefixDirsPsr4 = array (
'phpDocumentor\\Reflection\\' => array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
),
'Symfony\\Polyfill\\Mbstring\\' => array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Component\\Yaml\\' => array (
0 => __DIR__ . '/..' . '/symfony/yaml',
),
...)
PSR4 标准顶级命名空间映射用了两个数组,第一个是用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是 顶级命名空间的长度。为什么呢?
因为 PSR4 标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。
具体说明这些数组的作用:
假如我们找 Symfony\Polyfill\Mbstring\example
这个命名空间,通过前缀索引和字符串匹配我们得到了
<?php
'Symfony\\Polyfill\\Mbstring\\' => 26,
这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4数组
获取它的映射目录数组:(注意映射目录可能不止一条)
<?php
'Symfony\\Polyfill\\Mbstring\\' => array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
)
然后我们就可以将命名空间 Symfony\\Polyfill\\Mbstring\\example
前26个字符替换成目录 __DIR__ . '/..' . '/symfony/polyfill-mbstring
,我们就得到了__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php
,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。
第二种初始化方法,如果PHP版本低于 5.6 或者使用 HHVM 虚拟机环境,那么就要使用核心类的接口进行初始化。
<?php
// 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);
}
PSR4 标准的映射
autoload_psr4.php 的顶级命名空间映射
<?php
return array(
'XdgBaseDir\\'
=> array($vendorDir . '/dnoegel/php-xdg-base-dir/src'),
'Webmozart\\Assert\\'
=> array($vendorDir . '/webmozart/assert/src'),
'TijsVerkoyen\\CssToInlineStyles\\'
=> array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
'Tests\\'
=> array($baseDir . '/tests'),
'Symfony\\Polyfill\\Mbstring\\'
=> array($vendorDir . '/symfony/polyfill-mbstring'),
...
)
PSR4 标准的初始化接口:
<?php
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException(
"A non-empty PSR-4 prefix must end with a namespace separator."
);
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
总结下上面的顶级命名空间映射过程:
( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )
( 顶级命名空间 -> 目录 )
这两个映射数组。具体形式也可以查看下面的 autoload_static
的 、prefixDirsPsr4 。
命名空间映射
autoload_classmap:
<?php
public static $classMap = array (
'App\\Console\\Kernel'
=> __DIR__ . '/../..' . '/app/Console/Kernel.php',
'App\\Exceptions\\Handler'
=> __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
...
)
addClassMap:
<?php
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
自动加载核心类 ClassLoader 的静态初始化到这里就完成了!
初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。
第4部分:注册自动加载核心类对象
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 ‘App\Console\Kernel 中’ Console\Kernel 这一段转为目录,至于怎么转的在下面 “运行”的部分讲。核心类 ClassLoader 将 loadClass() 函数注册到PHP SPL中的 spl_autoload_register() 里面去。这样,每当PHP遇到一个不认识的命名空间的时候,PHP会自动调用注册到 spl_autoload_register 里面的 loadClass() 函数,然后找到命名空间对应的文件。
全局函数的自动加载
Composer 不止可以自动加载命名空间,还可以加载全局函数。怎么实现的呢?把全局函数写到特定的文件里面去,在程序运行前挨个 require就行了。这个就是 composer 自动加载的第五步,加载全局函数。
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
}
跟核心类的初始化一样,全局函数自动加载也分为两种:静态初始化和普通初始化,静态加载只支持PHP5.6以上并且不支持HHVM。
第5部分:运行
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative) {
return false;
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
}
return $file;
}
ClassLoader 的 register() 函数将 loadClass() 函数注册到 PHP 的 SPL 函数堆栈中,每当 PHP 遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以 loadClass() 函数就是自动加载的关键了。
总结
在分析laravel自动加载的核心源码后,发现其底层主要依靠spl_autoload_register(),include的方式,通过将类名,命名空间写成数组静态映射以此完成自动加载。
本博客参考了深入解析 composer 的自动加载原理
本作品采用《CC 协议》,转载必须注明作者和本文链接
所以这关 Laravel 什么事,而且不要随便修改别人的文章哦