启动
- Laravel 5.8
文章以 Laravel 学习。入口文件 public/index.php
:
// Register The Auto Loader
require __DIR__.'/../vendor/autoload.php';
autoload.php
不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。
autoload_real.php
中的类名为 ComposerAutoloaderInit...
这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。
其实还有一个做法我们更加熟悉,是定义一个命名空间。这里为什么不定义一个命名空间呢?一种理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。
autoload_real.php
autoload.php
主要调用了 getLoader()
:
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
自动加载机制注册了一个函数,这个函数 require
了 ClassLoader
文件。成功 new
出该文件中核心类 ClassLoader()
后,又销毁了该函数。
为什么不直接 require
?原因是:怕有的用户也定义了个 ComposerAutoloadClassLoader
命名空间,导致自动加载错误文件。
那为什么不跟引导类一样用个哈希值呢?原因是:这个类是可以复用的,框架允许用户使用这个类。
初始化核心类对象 3
对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。初始化的方法有两种:
- 使用
autoload_static
进行静态初始化 - 调用核心类接口初始化
autoload_static 静态初始化
静态初始化只支持 PHP 5.6
以上版本、不支持 HHVM
虚拟机、不存在 Zend-encoded file
。
autoload_static.php
<?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
中的 prefixLengthsPsr4
、prefixDirsPsr4
等等方法都是 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
,找到:
'P' => array (...)
这个数组,然后就会遍历这个数组来和 Parsedown/example
比较,发现第一个 Prophecy
不符合,第二个 Parsedown
符合,然后得到了映射目录(映射目录可能不止一个):
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
中 顶级命名空间
目录 直接加 到命名空间前面就可以得到路径:
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
而 PSR4
却是用顶级命名空间目录 替换 顶级命名空间,所以获得顶级命名空间的 长度 很重要:
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
举例:假如我们找 Symfony\Polyfill\Mbstring\example
这个类,和 PSR0
一样通过前缀索引和字符串匹配我们得到了:
'Symfony\Polyfill\Mbstring\' => 26,
这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4
获取它的映射目录数组(注意映射目录可能不止一条):
'Symfony\Polyfill\Mbstring\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
将 Symfony\Polyfill\Mbstring\example
前 26 个字母替换为 __DIR__ . '/..' . '/symfony/polyfill-mbstring
也就是:
__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php
先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。
自动加载核心类 ClassLoader
的静态初始化完成!
其实还有
$fallbackDirsPsr4
,暂未研究
调用接口初始化
如果 PHP
版本低于 5.6
或者使用 HHVM
虚拟机环境或者存在 zend_loader_file_encoded
,那么就要使用核心类的接口进行初始化。
/*
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
这一段。
/**
* 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);
}
一行代码实现自动加载。核心在 ClassLoader
的 loadClass()
函数上,这个函数负责按照 PSR
标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 AppConsoleKernel
中 ConsoleKernel
这一段转为目录。
自动加载全局函数 5
Composer
不止可以自动加载命名空间,还可以加载全局函数。就是把全局函数写到特定的文件里面去,在程序运行前挨个 require
就行了。
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
就不应该出现 $this
或 self
这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生:
- 第一种情况:引导类恰好有
foo()
函数,那么就会莫名其妙执行了引导类的foo()
。 - 第二种情况:引导类没有 foo() 函数,但是却甩出来引导类没有 foo() 方法这样的错误提示,用户不知道自己哪里错了。把 require 语句放到 引导类的外面,遇到 this 或者 self ,程序就会告诉用户根本没有类, this 或 self 无效,错误信息更加明朗。
问题 2
为什么要用 hash
作为 $fileIdentifier
?
这个变量是用来控制全局函数只被 require
一次的,那为什么不用 require_once
呢?事实上 require_once
比 require
效率低很多,使用全局变量 $GLOBALS
这样控制加载会更快。猜测另一个原因应该是 require_once
对相对路径的支持并不理想,所以 composer
尽量少用 require_once
。
运行
ClassLoader
将 loadClass()
函数注册到 PHP SPL
中的 spl_autoload_register()
里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register()
里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。
/**
* 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()
包含了PSR0
、PSR4
如果我们在代码中写 'phpDocumentorReflectionexample
,PHP 会通过 SPL 调用 loadClass
-> findFile
-> findFileWithExtension
。
- 首先默认用
.php
后缀名调用findFileWithExtension
函数里,利用PSR4
标准尝试解析目录文件,如果文件不存在则继续用PSR0
标准解析 - 如果解析出来的目录文件仍然不存在,但是环境是
HHVM
虚拟机,继续用后缀名.hh
再次调用findFileWithExtension
函数,如果不存在,说明此命名空间无法加载,放到classMap
中设为false
,以便以后更快地加载
PSR4
对于 phpDocumentorReflectionexample
,当尝试利用 PSR4
标准映射目录时,步骤如下:
// $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
标准映射目录时,步骤如下:
// $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
在加载类和加载全局方法时,都有两种方式。
$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