前言


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

注册


我们先回顾一下自动加载引导类:

public static function getLoader()
{
/***************************经典单例模式********************/
if (null !== self::$loader) {
return self::$loader;
} /***********************获得自动加载核心类对象********************/
spl_autoload_register(array('ComposerAutoloaderInit
832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit
832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader')); /***********************初始化自动加载核心类对象********************/
$useStaticLoader = PHP_VERSION_ID >= 50600 &&
!defined('HHVM_VERSION'); if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit
832ea71bfb9a4128da8660baedaac82e::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
832ea71bfb9a4128da8660baedaac82e::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
} foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire
832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
} return $loader;
}

现在我们开始引导类的第四部分:注册自动加载核心类对象。我们来看看核心类的 register() 函数:

public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

简单到爆炸啊!一行代码实现自动加载有木有!其实奥秘都在自动加载核心类 ClassLoader 的 loadClass() 函数上,这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 'App\Console\Kernel中'Console\Kernel 这一段转为目录,至于怎么转的我们在下面“Composer 自动加载源码分析——运行”讲。核心类 ClassLoader 将 loadClass() 函数注册到 PHP SPL 中的spl_autoload_register() 里面去,这个函数的来龙去脉我们之前 文章 讲过。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register 里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。

全局函数的自动加载

Composer 不止可以自动加载命名空间,还可以加载全局函数。怎么实现的呢?很简单,把全局函数写到特定的文件里面去,在程序运行前挨个 require 就行了。这个就是 composer 自动加载的第五步,加载全局函数。

if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
} foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
}

跟核心类的初始化一样,全局函数自动加载也分为两种:静态初始化和普通初始化,静态加载只支持 PHP5.6 以上并且不支持 HHVM。

静态初始化:

ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files:

public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
...
);

看到这里我们可能又要有疑问了,为什么不直接放文件路径名,还要一个 hash 干什么呢?这个我们一会儿讲,我们这里先了解一下这个数组的结构。

普通初始化

autoload_files:

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir); return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
....
);

其实跟静态初始化区别不大。

加载全局函数

class ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e{
public static function getLoader(){
...
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
}
...
}
} function composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file)
{
if (empty(\$GLOBALS['__composer_autoload_files'][\$fileIdentifier])) {
require $file; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

这一段很有讲究,
第一个问题:为什么自动加载引导类的 getLoader() 函数不直接 require includeFiles 里面的每个文件名,而要用类外面的函数composerRequire832ea71bfb9a4128da8660baedaac82e0?(顺便说下这个函数名 hash 仍然为了避免和用户定义函数冲突)因为怕有人在全局函数所在的文件写 this 或者self。
假如 includeFiles 有个 app/helper.php 文件,这个 helper.php 文件的函数外有一行代码:this->foo(),如果引导类在 getLoader() 函数直接 require(file),那么引导类就会运行这句代码,调用自己的 foo() 函数,这显然是错的。事实上 helper.php 就不应该出现 this 或 self 这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生,第一种情况:引导类恰好有 foo() 函数,那么就会莫名其妙执行了引导类的 foo();第二种情况:引导类没有 foo() 函数,但是却甩出来引导类没有 foo() 方法这样的错误提示,用户不知道自己哪里错了。把 require 语句放到引导类的外面,遇到 this 或者 self,程序就会告诉用户根本没有类,this 或 self 无效,错误信息更加明朗。

第二个问题,为什么要用 hash 作为 fileIdentifier,上面的代码明显可以看出来这个变量是用来控制全局函数只被 require 一次的,那为什么不用 require_once 呢?事实上require_once 比 require 效率低很多,使用全局变量 GLOBALS 这样控制加载会更快。

但是其实也带来了一些问题,如果存在两个自动加载,而且全局函数的相对路径不一致,很容易造成 hash 不相同,但是文件相同的情况,导致重复定义函数。所以在使用 composer 的时候最好要统一自动加载和依赖机制,最好不要多重自动加载。

运行

我们终于来到了核心的核心——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;
}
}

 

下面我们通过举例来说下上面代码的流程:
如果我们在代码中写下 'phpDocumentor\Reflection\example',PHP 会通过 SPL 调用 loadClass->findFile->findFileWithExtension。首先默认用 php 作为文件后缀名调用 findFileWithExtension 函数里,利用 PSR4 标准尝试解析目录文件,如果文件不存在则继续用 PSR0 标准解析,如果解析出来的目录文件仍然不存在,但是环境是 HHVM 虚拟机,继续用后缀名为 hh 再次调用 findFileWithExtension 函数,如果不存在,说明此命名空间无法加载,放到 classMap 中设为 false,以便以后更快地加载。
对于 phpDocumentor\Reflection\example,当尝试利用 PSR4 标准映射目录时,步骤如下:

PSR4 标准加载

  • 将 \ 转为文件分隔符 /,加上后缀 php 或 hh,得到 $logicalPathPsr4 即 phpDocumentor//Reflection//example.php(hh);

  • 利用命名空间第一个字母 p 作为前缀索引搜索 prefixLengthsPsr4 数组,查到下面这个数组:

p' =>
array (
'phpDocumentor\\Reflection\\' => 25,
'phpDocumentor\\Fake\\' => 19,
)
  • 遍历这个数组,得到两个顶层命名空间 phpDocumentor\Reflection\ 和 phpDocumentor\Fake\

  • 用这两个顶层命名空间与 phpDocumentor\Reflection\example_e 相比较,可以得到 phpDocumentor\Reflection\ 这个顶层命名空间

  • 在 prefixLengthsPsr4 映射数组中得到 phpDocumentor\Reflection\ 长度为25。

  • 在 prefixDirsPsr4 映射数组中得到 phpDocumentor\Reflection\ 的目录映射为:

'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
),
  • 遍历这个映射数组,得到三个目录映射;

  • 查看 “目录+文件分隔符 //+substr($logicalPathPsr4, $length)” 文件是否存在,存在即返回。这里就是 '__DIR__/../phpdocumentor/reflection-common/src + /+ substr(phpDocumentor/Reflection/example_e.php(hh),25)'

  • 如果失败,则利用 fallbackDirsPsr4 数组里面的目录继续判断是否存在文件,具体方法是“目录+文件分隔符//+$logicalPathPsr4”

PSR0 标准加载

如果 PSR4 标准加载失败,则要进行 PSR0 标准加载:

  • 找到 phpDocumentor\Reflection\example_e 最后“\”的位置,将其后面文件名中’‘_’‘字符转为文件分隔符“/”,得到 logicalPathPsr0 即 phpDocumentor/Reflection/example/e.php(hh)
    利用命名空间第一个字母 p 作为前缀索引搜索 prefixLengthsPsr4 数组,查到下面这个数组:

    'P' =>
array (
'Prophecy\\' =>
array (
0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
),
'phpDocumentor' =>
array (
0 => __DIR__ . '/..' . '/erusev/parsedown',
),
),
  • 遍历这个数组,得到两个顶层命名空间phpDocumentor和Prophecy

  • 用这两个顶层命名空间与 phpDocumentor\Reflection\example_e 相比较,可以得到 phpDocumentor 这个顶层命名空间

  • 在映射数组中得到 phpDocumentor 目录映射为 '_DIR_ . '/..' . '/erusev/parsedown'

  • 查看 “目录+文件分隔符//+logicalPathPsr0”文件是否存在,存在即返回。这里就是
    “_DIR_ . '/..' . '/erusev/parsedown + //+ phpDocumentor//Reflection//example/e.php(hh)”

  • 如果失败,则利用 fallbackDirsPsr0 数组里面的目录继续判断是否存在文件,具体方法是“目录+文件分隔符//+logicalPathPsr0”

  • 如果仍然找不到,则利用 stream_resolve_include_path(),在当前 include 目录寻找该文件,如果找到返回绝对路径。

结语

经过三篇文章,终于写完了 PHP Composer 自动加载的原理与实现,结下来我们开始讲解 laravel 框架下的门面 Facade,这个门面功能和自动加载有着一些联系.

本文转自:https://segmentfault.com/a/1190000009369315

Composer的Autoload源码实现2——注册与运行的更多相关文章

  1. Composer的Autoload源码实现1——启动与初始化

    前言 上一篇文章,我们讨论了 PHP 的自动加载原理.PHP 的命名空间.PHP 的 PSR0 与 PSR4 标准,有了这些知识,其实我们就可以按照 PSR4 标准写出可以自动加载的程序了.然而我们为 ...

  2. 30、[源码]-AOP原理-注册AnnotationAwareAspectJAutoProxyCreavi

    30.[源码]-AOP原理-注册AnnotationAwareAspectJAutoProxyCreavi

  3. 转:[gevent源码分析] 深度分析gevent运行流程

    [gevent源码分析] 深度分析gevent运行流程 http://blog.csdn.net/yueguanghaidao/article/details/24281751 一直对gevent运行 ...

  4. 源码深度解析SpringMvc请求运行机制(转)

    源码深度解析SpringMvc请求运行机制 本文依赖的是springmvc4.0.5.RELEASE,通过源码深度解析了解springMvc的请求运行机制.通过源码我们可以知道从客户端发送一个URL请 ...

  5. 老李推荐:第8章7节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-小结

    老李推荐:第8章7节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-小结   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性 ...

  6. 老李推荐:第8章5节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-运行测试脚本

    老李推荐:第8章5节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-运行测试脚本   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化 ...

  7. 老李推荐:第8章1节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-运行环境初始化

    老李推荐:第8章1节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-运行环境初始化   首先大家应该清楚的一点是,MonkeyRunner的运行是牵涉到主机端和目 ...

  8. Ubuntu TensorFlow 源码 Android Demo的编译运行

    Ubuntu TensorFlow 源码 Android Demo的编译运行 一. 安装 Android 的SDK和NDK SDK 配置 A:下载 国内下载地址选最新的: SDK: https://d ...

  9. Spark源码分析之八:Task运行(二)

    在<Spark源码分析之七:Task运行(一)>一文中,我们详细叙述了Task运行的整体流程,最终Task被传输到Executor上,启动一个对应的TaskRunner线程,并且在线程池中 ...

随机推荐

  1. matlab与C++以.mat文件方式进行数据相互流动

    年前,放假回家之前,使用了C++与matlab之间的数据的互动的一个实验,感觉效果挺好.初步达到了目的,所以整理下来方便大家使用.减少大家编程学习的时间.希望对你们有用. #include " ...

  2. [置顶] kubernetes资源对象--ConfigMap

    原理 很多生产环境中的应用程序配置较为复杂,可能需要多个config文件.命令行参数和环境变量的组合.使用容器部署时,把配置应该从应用程序镜像中解耦出来,以保证镜像的可移植性.尽管Secret允许类似 ...

  3. 自助采样法 bootstrap 与 0.632

  4. CKEditor+SWFUpload实现功能较为强大的编辑器(三)---后台接收图片流程

    在前台配置完CKEditor和SWFUpload之后就可以满足基本的需求了 在这里,我配置的接收异步上传的图片的页面为upload.ashx 在这个ashx中对上传的图片处理的流程如下: contex ...

  5. RabbitMQ三----'任务分发 '

    当有Consumer需要大量的运算时,RabbitMQ Server需要一定的分发机制来balance每个Consumer的load.试想一下,对于web application来说,在一个很多的HT ...

  6. psql命令行快速参考

    psql的命令语法是: psql [options] [dbname [username]] psql命令行选项以及它们的意思在表1-1中列出.使用以下命令可以看到psql完整的选项列表: $ psq ...

  7. JavaScript数组归并方法reduce

    示例代码: <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF ...

  8. 杭电 HDU 2717 Catch That Cow

    Catch That Cow Time Limit: 5000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) T ...

  9. Oracle 导入导出数据库

    imp userid=yrsuser/yrsuser2587 fromuser=yrsuser touser=yrsuser file=E:\yrs.dmp exp userid=yrsuser/yr ...

  10. iOS应用程序开发之内购

    内购简介 配置iTunes Connect iOS客户端开发工作 一.内购简介 1⃣️通过苹果应用程序商店有三种主要赚钱的方式: –直接收费(与国内大部分用户的消费习惯相悖,如果直接收费,不要设置为6 ...