很多框架在初始化的时候都会引入 composer 来协助自动加载的,以 Laravel 为例,它入口文件 index.php 第一句就是利用 composer 来实现自动加载功能。
启动
<?php
define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
去 vendor 目录下的 autoload.php :
<?php
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit39453c09913b57d643e4205aada7ab4::getLoader();
我把自动加载引导类分为 5 个部分。我把自动加载引导类分为 5 个部分。
public static function getLoader()
{
/***************************经典单例模式********************/
if (null !== self::$loader) {
return self::$loader;
}
/***********************获得自动加载核心类对象********************/
spl_autoload_register(array('ComposerAutoloaderInit39453c09913b57d643e4205aada7ab4', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit39453c09913b57d643e4205aada7ab4', 'loadClassLoader'));
/***********************初始化自动加载核心类对象********************/
$useStaticLoader = PHP_VERSION_ID >= 50600 &&
!defined('HHVM_VERSION');
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit
7b790917ce8899df9af8ed53631a1c29::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\ComposerStaticInit39453c09913b57d643e4205aada7ab4::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire39453c09913b57d643e4205aada7ab4($fileIdentifier, $file);
}
return $loader;
}
第一部分——单例
第一部分很简单,就是个最经典的单例模式,自动加载类只能有一个。
<?php
if (null !== self::$loader) {
return self::$loader;
}
第二部分——构造ClassLoader核心类
new 一个自动加载的核心类对象。
<?php
/***********************获得自动加载核心类对象********************/
spl_autoload_register(
array('ComposerAutoloaderInit39453c09913b57d643e4205aada7ab4', 'loadClassLoader'), true, true
);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(
array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader')
);
loadClassLoader()函数:
<?php
public static function loadClassLoader($class){
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
从程序里面我们可以看出,composer 先向 PHP 自动加载机制注册了一个函数,这个函数 require 了 ClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。
第三部分 —— 初始化核心类对象
这一部分就是对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。
<?php
/***********************初始化自动加载核心类对象********************/
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
if ($useStaticLoader) { //使用 autoload_static 进行静态初始化;
require_once __DIR__ . '/autoload_static.php';
call_user_func(
\Composer\Autoload\ComposerStaticInite39453c09913b57d643e4205aada7ab4::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);
}
}
autoload_static 静态初始化 ( PHP >= 5.6 )
静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机。我们深入 autoload_static.php 这个文件发现这个文件定义了一个用于静态初始化的类,名字叫ComposerStaticInite39453c09913b57d643e4205aada7ab4,仍然为了避免冲突而加了 hash 值。这个类很简单:
<?php
class ComposerStaticInite39453c09913b57d643e4205aada7ab4{
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
= ComposerStaticInite39453c09913b57d643e4205aada7ab4::$prefixLengthsPsr4;
$loader->prefixDirsPsr4
= ComposerStaticInite39453c09913b57d643e4205aada7ab4::$prefixDirsPsr4;
$loader->prefixesPsr0
= ComposerStaticInite39453c09913b57d643e4205aada7ab4::$prefixesPsr0;
$loader->classMap
= ComposerStaticInite39453c09913b57d643e4205aada7ab4::$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,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。
第四部分 —— 注册
讲完了 Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,也就是说,如果有命名空间 'App\Console\Kernel,我们已经可以找到它对应的类文件所在位置。那么,它是什么时候被触发去找的呢?
这就是 composer 自动加载的核心了,我们先回顾一下自动加载引导类:
public static function getLoader()
{
/***************************经典单例模式********************/
if (null !== self::$loader) {
return self::$loader;
}
/***********************获得自动加载核心类对象********************/
spl_autoload_register(array('ComposerAutoloaderInit
7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit
7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'));
/***********************初始化自动加载核心类对象********************/
$useStaticLoader = PHP_VERSION_ID >= 50600 &&
!defined('HHVM_VERSION');
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit
7b790917ce8899df9af8ed53631a1c29::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\ComposerStaticInit
7b790917ce8899df9af8ed53631a1c29::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire
7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
}
return $loader;
}
现在我们开始引导类的第四部分:注册自动加载核心类对象。我们来看看核心类的 register() 函数:
public function register($prepend = false){
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
全局函数的自动加载
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);
}
第五部分 —— 运行
到这里,终于来到了核心的核心—— composer 自动加载的真相,命名空间如何通过 composer 转为对应目录文件的奥秘就在这一章。
前面说过,ClassLoader 的 register() 函数将 loadClass() 函数注册到 PHP 的 SPL 函数堆栈中,每当 PHP 遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以 loadClass() 函数就是自动加载的关键了。
看下 loadClass() 函数:
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;
}
我们看到 loadClass() ,主要调用 findFile() 函数。findFile() 在解析命名空间的时候主要分为两部分:classMap 和 findFileWithExtension() 函数。classMap 很简单,直接看命名空间是否在映射数组中即可。麻烦的是 findFileWithExtension() 函数,这个函数包含了 PSR0 和 PSR4 标准的实现。还有个值得我们注意的是查找路径成功后 includeFile() 仍然是外面的函数,并不是 ClassLoader 的成员函数,原理跟上面一样,防止有用户写 $this 或 self。还有就是如果命名空间是以\开头的,要去掉\然后再匹配。
看下 findFileWithExtension 函数:
private function findFileWithExtension($class, $ext){
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// 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;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
}