PHP Composer自动加载

思考并回答以下问题:

  • 在__autoload()函数中将所有的映射规则全部实现,可能会非常复杂。为什么?
  • __autoload()是全局函数只能定义一次,不够灵活,所以所有的类名与文件名对应的逻辑规则都要在一个函数里面实现,造成这个函数的臃肿。怎么解决?

PHP自动加载功能

由来

在PHP开发过程中,如果希望从外部引入一个Class,通常会使用include和require方法,去把定义这个Class的文件包含进来。这个在小规模开发的时候,没什么大问题。但在大型的开发项目中,使用这种方式会带来一些隐含的问题:如果一个PHP文件需要使用很多其它类,那么就需要很多的require/include语句,这样有可能会造成遗漏或者包含进不必要的类文件。如果大量的文件都需要使用其它的类,那么要保证每个文件都包含正确的类文件肯定是一个噩梦,况且require_once的代价很大。

PHP5为这个问题提供了一个解决方案,这就是类的自动加载(autoload)机制。autoload机制可以使得PHP程序有可能在使用类时才自动包含类文件,而不是一开始就将所有的类文件include进来,这种机制也称为Lazy Loading(延迟加载)。

总结起来,自动加载功能带来了几处优点:

  • 1.使用类之前无需include/require。
  • 2.使用类的时候才会include/require文件,实现了Lazy Loading,避免了include/require多余文件。
  • 3.无需考虑引入类的实际磁盘地址,实现了逻辑和实体文件的分离。

PHP自动加载函数__autoload()

通常PHP5在使用一个类时,如果发现这个类没有加载,就会自动运行__autoload()函数,这个函数是我们在程序中自定义的,在这个函数中我们可以加载需要使用的类。下面是个简单的示例:

1
2
3
4
5
6
<?php

function __autoload($classname)
{
require_once ($classname . ".class.php");
}

在我们这个简单的例子中,我们直接将类名加上扩展名.class.php构成了类文件名,然后使用require_once将其加载。

从这个例子中,我们可以看出__autoload至少要做三件事情:

  • 1.根据类名确定类文件名;
  • 2.确定类文件所在的磁盘路径(在我们的例子是最简单的情况,类与调用它们的PHP程序文件在同一个文件夹下);
  • 3.将类从磁盘文件中加载到系统中。

第三步最简单,只需要使用include/require即可。要实现第一步,第二步的功能,必须在开发时约定类名与磁盘文件的映射方法,只有这样我们才能根据类名找到它对应的磁盘文件。

当有大量的类文件要包含的时候,我们只要确定相应的规则,然后在__autoload()函数中,将类名与实际的磁盘文件对应起来,就可以实现Lazy Loading的效果。从这里我们也可以看出__autoload()函数的实现中最重要的是类名与实际的磁盘文件映射规则的实现

__autoload()函数存在的问题

如果在一个系统的实现中,需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的,其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须在__autoload()函数中将所有的映射规则全部实现,这样的话__autoload()函数有可能会非常复杂,甚至无法实现。最后可能会导致__autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。

那么问题出现在哪里呢?问题出现在__autoload()是全局函数只能定义一次,不够灵活,所以所有的类名与文件名对应的逻辑规则都要在一个函数里面实现,造成这个函数的臃肿。那么如何来解决这个问题呢?答案就是使用一个__autoload调用堆栈,不同的映射关系写到不同的__autoload函数中去,然后统一注册统一管理,这个就是PHP5引入的SPL Autoload。

SPL Autoload

SPL是Standard PHP Library(标准PHP库)的缩写。它是PHP5引入的一个扩展库,其主要功能包括autoload机制的实现及包括各种Iterator接口或类。SPL Autoload具体有几个函数:

  • 1.spl_autoload_register:注册__autoload()函数
  • 2.spl_autoload_unregister:注销已注册的函数
  • 3.spl_autoload_functions:返回所有已注册的函数
  • 4.spl_autoload_call:尝试所有已注册的函数来加载类
  • 5.spl_autoload:__autoload()的默认实现
  • 6.spl_autoload_extionsions:注册并返回spl_autoload函数使用的默认文件扩展名。

简单来说,spl_autoload就是SPL自己定义的__autoload()函数,功能很简单,就是去注册的目录(由set_include_path设置)找与$classname同名的.php/.inc文件。当然,你也可以指定特定类型的文件,方法是注册扩展名(spl_autoload_extionsions)。

而splautoload_register()就是我们上面所说的__autoload调用堆栈,我们可以向这个函数注册多个我们自己的__autoload()函数,当PHP找不到类名时,PHP就会调用这个堆栈,一个一个去调用自定义的__autoload()函数,实现自动加载功能。如果我们不向这个函数输入任何参数,那么就会注册spl_autoload()函数。

好啦,PHP自动加载的底层就是这些,注册机制已经非常灵活,但是还缺什么呢?我们上面说过,自动加载关键就是类名和文件的映射,这种映射关系不同框架有不同方法,非常灵活,但是过于灵活就会显得杂乱,PHP有专门对这种映射关系的规范,那就是PSR标准中PSR0与PSR4。

不过在谈PSR0与PSR4之前,我们还需要了解PHP的命名空间的问题,因为这两个标准其实针对的都不是类名与目录文件的映射,而是命名空间与文件的映射。

为什么会这样呢?在我的理解中,规范的面向对象PHP思想,命名空间在一定程度上算是类名的别名,那么为什么要推出命名空间,命名空间的优点是什么呢?

二、Namespace命名空间

什么是命名空间?从广义上来说,命名空间是一种封装事物的方法。在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组,对于目录中的文件来说,它就扮演了命名空间的角色。具体举个例子,文件foo.txt可以同时在目录/home/greg和/home/other中存在,但在同一个目录中不能存在两个foo.txt文件。另外,在目录/home/greg外访问foo.txt文件时,我们必须将目录名以及目录分隔符放在文件名之前得到/home/greg/foo.txt。这个原理应用到程序设计领域就是命名空间的概念。

在PHP中,命名空间用来解决在编写类库或应用程序时创建可重用的代码如类或函数时碰到的两类问题:

  • 1.用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。
  • 2.为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。

PHP命名空间提供了一种将相关的类、函数和常量组合到一起的途径。

简单来说就是PHP是不允许程序中存在两个名字一样一样的类或者函数或者变量名的,那么有人就很疑惑了,那就不起一样名字不就可以了?事实上很多大程序依赖很多第三方库,名字冲突什么的不要太常见,这个就是官网中的第一个问题。那么如何解决这个问题呢?在没有命名空间的时候,可怜的程序员只能给类名起 a_b_c_d_e_f 这样的,其中 a/b/c/d/e/f 一般有其特定意义,这样一般就不会发生冲突了,但是这样长的类名编写起来累,读起来更是难受。因此PHP5就推出了命名空间,类名是类名,命名空间是命名空间,程序写/看的时候直接用类名,运行起来机器看的是命名空间,这样就解决了问题。

另外,命名空间提供了一种将相关的类、函数和常量组合到一起的途径。这也是面向对象语言命名空间的很大用途,把特定用途所需要的类、变量、函数写到一个命名空间中,进行封装。

解决了类名的问题,我们终于可以回到PSR标准来了,那么PSR0与PSR4是怎么 规范 文件与命名空间的映射关系的呢?答案就是:对命名空间的命名(额,有点绕)、类文件目录的位置 和 两者映射关系 做出了限制,这个就是标准的核心了。

三、PSR标准

PSR是Proposing a Standards Recommendation(提出标准建议)的缩写,截止到目前为止,总共有5套PSR规范,分别是:

PSR-0 (Autoloading Standard) 自动加载标准
PSR-1 (Basic Coding Standard)基础编码标准
PSR-2 (Coding Style Guide) 编码风格向导
PSR-3 (Logger Interface) 日志接口
PSR-4 (Improved Autoloading) 自动加载的增强版,可以替换掉PSR-0了。

PSR0标准

PRS-0规范是他们出的第1套规范,主要是制定了一些自动加载标准(Autoloading Standard)PSR-0强制性要求几点:

  • 1.一个完全合格的namespace和class必须符合这样的结构:
1
<VendorName>[<Namespace>]*<Class Name>
  • 2.每个namespace必须有一个顶层的namespace(”Vendor Name”提供者名字)
  • 3.每个namespace可以有多个子namespace
  • 4.当从文件系统中加载时,每个namespace的分隔符(/)要转换成DIRECTORY_SEPARATOR(操作系统路径分隔符)
  • 5.在类名中,每个下划线(_) 符号要转换成DIRECTORY_SEPARATOR(操作系统路径分隔符)。在 namespace 中,下划线 _ 符号是没有(特殊)意义的。
  • 6.当从文件系统中载入时,合格的 namespace 和 class 一定是以.php结尾的
  • 7.verdor name , namespaces , class 名可以由大小写字母组合而成(大小写敏感的)

具体规则可能有些让人晕,我们从头讲一下。

我们先来看PSR0标准大致内容,第1、2、3、7条对命名空间的名字做出了限制,第4、5条对命名空间和文件目录的映射关系做出了限制,第6条是文件后缀名。

前面我们说过,PSR标准是如何规范命名空间和所在文件目录之间的映射关系?是通过限制命名空间的名字、所在文件目录的位置和两者映射关系。

那么我们可能就要问了,哪里限制了文件所在目录的位置了呢?其实答案就是:

限制命名空间名字 + 限制命名空间名字与文件目录映射 = 限制文件目录

好了,我们先想一想,对于一个具体程序来说,如果它想要支持PSR0标准,它需要做什么调整呢?

  1. 首先,程序必须定义一个符合PSR0标准第4、5条的映射函数,然后把这个函数注册到spl_register()中;

  2. 其次,定义一个新的命名空间时,命名空间的名字和所在文件的目录位置必须符合第1、2、3、7条。

一般为了代码维护方便,我们会在一个文件只定义一个命名空间。

好了,我们有了符合PSR0的命名空间的名字,通过符合PSR0标准的映射关系就可以得到符合PSR0标准的文件目录地址,如果我们按照PSR0标准正确存放文件,就可以顺利require该文件了,我们就可以使用该命名空间啦,是不是很神奇呢?

接下来,我们详细地来看看PSR0标准到底规范了什么呢?

我们以Laravel中第三方库Symfony其中一个命名空间 /Symfony/Core/Request为例,讲一讲上面PSR0标准。

  1. 一个完全合格的namespace和class必须符合这样的结构:
    1
    <VendorName>[<Namespace>]*<Class Name>

上面所展示的/Symfony就是Vendor Name,也就是第三方库的名字,/Core是Namespace名字,一般是我们命名空间的一些属性信息(例如request是Symfony的核心功能);最后Request就是我们命名空间的名字,这个标准规范就是让人看到命名空间的来源、功能非常明朗,有利于代码的维护。

2 . 每个namespace必须有一个顶层的namespace(”Vendor Name”提供者名字)

也就是说每个命名空间都要有一个类似于/Symfony的顶级命名空间,为什么要有这种规则呢?因为PSR0标准只负责顶级命名空间之后的映射关系,也就是/Symfony/Core/Request这一部分,关于/Symfony应该关联到哪个目录,那就是用户或者框架自己定义的了。所谓的顶层的namespace,就是自定义了映射关系的命名空间,一般就是提供者名字(第三方库的名字)。换句话说顶级命名空间是自动加载的基础。为什么标准要这么设置呢?原因很简单,如果有个命名空间是/Symfony/Core/Transport/Request,还有个命名空间是/Symfony/Core/Transport/Request1,如果没有顶级命名空间,我们就得写两个路径和这两个命名空间相对应,如果再有Request2、Request3呢。有了顶层命名空间/Symfony,那我们就仅仅需要一个目录对应即可,剩下的就利用PSR标准去解析就行了。

3.每个namespace可以有多个子namespace

这个很简单,Request可以定义成/Symfony/Core/Request,也可以定义成/Symfony/Core/Transport/Request,/Core这个命名空间下面可以有很多子命名空间,放多少层命名空间都是自己定义。

4.当从文件系统中加载时,每个namespace的分隔符(/)要转换成DIRECTORY_SEPARATOR(操作系统路径分隔符)

现在我们终于来到了映射规范了。命名空间的/符号要转为路径分隔符,也就是说要把/Symfony/Core/Request这个命名空间转为\Symfony\Core\Request这样的目录结构。

5.在类名中,每个下划线_符号要转换成DIRECTORYSEPARATOR(操作系统路径分隔符)。在namespace中,下划线\符号是没有(特殊)意义的。

这句话的意思就是说,如果我们的命名空间是/Symfony/Core/Request_a,那么我们就应该把它映射到\Symfony\Core\Request\a这样的目录。为什么会有这种规定呢?这是因为PHP5之前并没有命名空间,程序员只能把名字起成Symfony_Core_Request_a这样,PSR0的这条规定就是为了兼容这种情况。

剩下两个很简单就不说了。
有这样的命名空间命名规则和映射标准,我们就可以推理出我们应该把命名空间所在的文件该放在哪里了。依旧以Symfony/Core/Request为例, 它的目录是/path/to/project/vendor/Symfony/Core/Request.php,其中/path/to/project是你项目在磁盘的位置,/path/to/project/vendor是项目用的所有第三方库所在目录。/path/to/project/vendor/Symfony就是与顶级命名空间/Symfony存在对应关系的目录,再往下的文件目录就是按照PSR0标准建立的:
/Symfony/Core/Request => /Symfony/Core/Request.php
一切很完满了是吗?不,还有一些瑕疵:

  1. 我们是否应该还兼容没有命名空间的情况呢?
  2. 按照PSR0标准,命名空间/A/B/C/D/E/F必然对应一个目录结构/A/B/C/D/E/F,这种目录结构层次是不是太深了?

PSR4标准

2013年底,新出了第5个规范——PSR-4。

PSR-4规范了如何指定文件路径从而自动加载类定义,同时规范了自动加载文件的位置。这个乍一看和PSR-0重复了,实际上,在功能上确实有所重复。区别在于PSR-4的规范比较干净,去除了兼容PHP 5.3以前版本的内容,有一点PSR-0升级版的感觉。当然,PSR-4也不是要完全替代PSR-0,而是在必要的时候补充PSR-0——当然,如果你愿意,PSR-4也可以替代PSR-0。PSR-4可以和包括PSR-0在内的其他自动加载机制共同使用。

PSR4标准与PSR0标准的区别:

  1. 在类名中使用下划线没有任何特殊含义。
  2. 命名空间与文件目录的映射方法有所调整。

对第二项我们详细解释一下(Composer自动加载的原理):

假如我们有一个命名空间:Foo/class,Foo是顶级命名空间,其存在着用户定义的与目录的映射关系:
“Foo/“ => “src/“
  按照PSR0标准,映射后的文件目录是:src/Foo/class.php,但是按照PSR4标准,映射后的文件目录就会是:src/class.php,为什么要这么更改呢?原因就是怕命名空间太长导致目录层次太深,使得命名空间和文件目录的映射关系更加灵活。

再举一个例子,来源PSR-4——新鲜出炉的PHP规范:

PSR-0风格

1
2
3
4
5
6
7
8
9
10
11
-vendor/
| -vendor_name/
| | -package_name/
| | | -src/
| | | | -Vendor_Name/
| | | | | -Package_Name/
| | | | | | -ClassName.php # Vendor_Name\Package_Name\ClassName
| | | -tests/
| | | | -Vendor_Name/
| | | | | -Package_Name/
| | | | | | -ClassNameTest.php # Vendor_Name\Package_Name\ClassName

PSR-4风格

1
2
3
4
5
6
7
-vendor/
| -vendor_name/
| | -package_name/
| | | -src/
| | | | -ClassName.php # Vendor_Name\Package_Name\ClassName
| | | -tests/
| | | | -ClassNameTest.php # Vendor_Name\Package_Name\ClassNameTest

对比以上两种结构,明显可以看出PSR-4带来更简洁的文件结构。

前言

上一篇文章中,我们讨论了PHP自动加载功能、PHP命名空间、PSR0/PSR4标准,有了这些知识,其实我们就可以按照PSR4标准写出可以自动加载的程序了。然而我们为什么要自己写呢?尤其是有Composer这神一样的包管理器的情况下?

Composer自动加载概论

简单的说,Composer 帮助我们下载好了符合PSR0/PSR4标准的第三方库,并把文件放在相应位置;帮我们写了__autoload()函数,注册到了spl_register()函数,当我们想用第三方库的时候直接使用命名空间即可。

那么当我们想要写自己的命名空间的时候,该怎么办呢?很简单,我们只要按照PSR4标准命名我们的命名空间,放置我们的文件,然后在composer里面写好顶级域名与具体目录的映射,就可以享用composer的便利了。

当然如果有一个非常棒的框架,我们会惊喜地发现,在 composer 里面写顶级域名映射这事我们也不用做了,框架已经帮我们写好了顶级域名映射了,我们只需要在框架里面新建文件,在新建的文件中写好命名空间,就可以在任何地方 use 我们的命名空间了。

下面我们就以 Laravel 框架为例,讲一讲composer是如何实现PSR0/PSR4标准的自动加载功能。
Composer自动加载文件

首先,我们先大致了解一下Composer自动加载所用到的源文件。

  1. autoload_real.php: 自动加载功能的引导类。
    任务是composer加载类的初始化 (顶级命名空间与文件路径映射初始化) 和注册(spl_autoload_register())。
  2. ClassLoader.php: composer加载类。
    composer自动加载功能的核心类。
  3. autoload_static.php: 顶级命名空间初始化类,
    用于给核心类初始化顶级命名空间。
  4. autoload_classmap.php: 自动加载的最简单形式,
    有完整的命名空间和文件目录的映射;
  5. autoload_files.php: 用于加载全局函数的文件,
    存放各个全局函数所在的文件路径名;
  6. autoload_namespaces.php: 符合PSR0标准的自动加载文件,
    存放着顶级命名空间与文件的映射;
  7. autoload_psr4.php: 符合PSR4标准的自动加载文件,
    存放着顶级命名空间与文件的映射;

Laravel框架下Composer的自动加载源码分析

启动

Laravel框架的初始化是需要composer自动加载协助的,所以Laravel的入口文件index.php利用composer来实现自动加载功能。

1
2
3
4
5
<?php

define('LARAVEL_START', microtime(true));

require __DIR__.'/../vendor/autoload.php';

再去vendor目录下的autoload.php:

1
2
3
4
<?php
require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInit41730e5b8bc2247776726758bb07e21b::getLoader();

好了,我们终于要看到了Composer真正要显威的地方了。autoload_real.php里面就是一个自动加载功能的引导类,这个类不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。

到autoload_real这个文件里面去看,发现这个引导类的名字叫ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e,为什么要叫这么古怪的名字呢?因为这是防止用户自定义类名跟这个类重复冲突了,所以在类名上加了一个hash值。其实还有一个做法我们更加熟悉,那就是不直接定义类名,而是定义一个命名空间。这里为什么不定义一个命名空间呢?个人理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用hash值更加合适。

Laravel框架下Composer的自动加载源码分析-autoload_real引导类

在vendor目录下的autoload.php文件中我们可以看出,程序主要调用了引导类的静态方法getLoader(),我们接着看看这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static function getLoader()
{
if (null !== self::$loader)
{
return self::$loader;
}

spl_autoload_register(array('ComposerAutoloaderInit41730e5b8bc2247776726758bb07e21b', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit41730e5b8bc2247776726758bb07e21b', '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\ComposerStaticInit41730e5b8bc2247776726758bb07e21b::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\ComposerStaticInit41730e5b8bc2247776726758bb07e21b::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire41730e5b8bc2247776726758bb07e21b($fileIdentifier, $file);
}

return $loader;
}

从上面可以看出,我把自动加载引导类分为5个部分。

第一部分—单例

第一部分很简单,就是个最经典的单例模式,自动加载类只能有一个。

1
2
3
4
5
<?php
if (null !== self::$loader)
{
return self::$loader;
}

第二部分——构造ClassLoader核心类

第二部分new一个自动加载的核心类对象。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
/***********************获得自动加载核心类对象********************/
spl_autoload_register(
array('ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82
e', 'loadClassLoader'), true, true
);
self::$loader = $loader = new \Composer\Autoload\ClassLoader()
;
spl_autoload_unregister(
array('ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82
e', 'loadClassLoader')
);

loadClassLoader()函数:

1
2
3
4
5
6
7
<?php
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

从程序里面我们可以看出,composer先向PHP自动加载机制注册了一个函数,这个函数require了 ClassLoader 文件。成功new出该文件中核心类ClassLoader()后,又销毁了该函数。

为什么不直接require,而要这么麻烦?原因就是怕有的用户也定义了个\Composer\Autoload\ClassLoader命名空间,导致自动加载错误文件。那为什么不跟引导类一样用个hash呢?因为这个类是可以复用的,框架允许用户使用这个类。

第三部分-初始化核心类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
/***********************初始化自动加载核心类对象******************
**/
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_V
ERSION');
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(
\Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da86
60baedaac82e::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);
}
}

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

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

autoload_static静态初始化

静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机。我们深入autoload_static.php 这个文件发现这个文件定义了一个用于静态初始化的类,名字叫ComposerStaticInit832ea71bfb9a4128da8660baedaac82e ,仍然为了避免冲突加了 hash 值。这个类很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class ComposerStaticInit832ea71bfb9a4128da8660baedaac82e{
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
= ComposerStaticInit832ea71bfb9a4128da
8660baedaac82e::$prefixLengthsPsr4;
$loader->prefixDirsPsr4
= ComposerStaticInit832ea71bfb9a4128da
8660baedaac82e::$prefixDirsPsr4;
$loader->prefixesPsr0
= ComposerStaticInit832ea71bfb9a4128da
8660baedaac82e::$prefixesPsr0;
$loader->classMap
= ComposerStaticInit832ea71bfb9a4128da
8660baedaac82e::$classMap;
}, null, ClassLoader::class);
}

这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了ClassLoader类。值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是ClassLoader类中的prefixLengthsPsr4、prefixDirsPsr4等等方法都是private的。。。普通的函数没办法给类的private成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为ClassLoader类 的成员函数。

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

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

最简单的 classMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
public static $classMap = array (
'App\\Console\\Kernel'
=> __DIR__ . '/../..' . '/app/Console/Kernel.php',
'App\\Exceptions\\Handler'
=> __DIR__ . '/../..' . '/app/Exceptions/Handler.p
hp',
'App\\Http\\Controllers\\Auth\\ForgotPasswordController'
=> __DIR__ . '/../..' . '/app/Http/Controllers/Aut
h/ForgotPasswordController.php',
'App\\Http\\Controllers\\Auth\\LoginController'
=> __DIR__ . '/../..' . '/app/Http/Controllers/Aut
h/LoginController.php',
'App\\Http\\Controllers\\Auth\\RegisterController'
=> __DIR__ . '/../..' . '/app/Http/Controllers/Aut
h/RegisterController.php',
...)

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

PSR0 顶级命名空间映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
public static $prefixesPsr0 = array (
'P' => array (
'Prophecy\\' => array (
0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
),
'Parsedown' => array (
0 => __DIR__ . '/..' . '/erusev/parsedown',
),
),
'M' => array (
'Mockery' => array (
0 => __DIR__ . '/..' . '/mockery/mockery/library',
),
),
'J' => array (
'JakubOnderka\\PhpConsoleHighlighter' => array (
0 => __DIR__ . '/..' . '/jakub-onderka/php-console-h
ighlighter/src',
),
'JakubOnderka\\PhpConsoleColor' => array (
0 => __DIR__ . '/..' . '/jakub-onderka/php-console-c
olor/src',
),
),
'D' => array (
'Doctrine\\Common\\Inflector\\' => array (
0 => __DIR__ . '/..' . '/doctrine/inflector/lib',
),
),
);

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

1
2
3
4
5
6
7
8
9
<?php
'P' => array (
'Prophecy\\' => array (
0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
),
'Parsedown' => array (
0 => __DIR__ . '/..' . '/erusev/parsedown',
),
),

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

1
2
<?php
array (0 => __DIR__ . '/..' . '/erusev/parsedown',)

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

PSR4标准顶级命名空间映射数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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-docblo
ck/src',
),
'Symfony\\Polyfill\\Mbstring\\' => array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Component\\Yaml\\' => array (
0 => __DIR__ . '/..' . '/symfony/yaml',
),
...)

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一样通过前缀索引和字符串匹配我们得到了

1
2
<?php
'Symfony\\Polyfill\\Mbstring\\' => 26,

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

1
2
3
4
5
<?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,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。

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

ClassLoader接口初始化

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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);
}

PSR0标准
autoload_namespaces:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
return array(
'Prophecy\\'
=> array($vendorDir . '/phpspec/prophecy/src'),
'Parsedown'
=> array($vendorDir . '/erusev/parsedown'),
'Mockery'
=> array($vendorDir . '/mockery/mockery/library'),
'JakubOnderka\\PhpConsoleHighlighter'
=> array($vendorDir . '/jakub-onderka/php-console-hi
ghlighter/src'),
'JakubOnderka\\PhpConsoleColor'
=> array($vendorDir . '/jakub-onderka/php-console-co
lor/src'),
'Doctrine\\Common\\Inflector\\'
=> array($vendorDir . '/doctrine/inflector/lib'),
);

PSR0标准的初始化接口:

1
2
3
4
5
6
7
8
9
10
<?php
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $
paths;
}
}

很简单,PSR0标准取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面我们讲的autoload_static的$prefixesPsr0。如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。
PSR4标准
autoload_psr4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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-style
s/src'),
'Tests\\'
=> array($baseDir . '/tests'),
'Symfony\\Polyfill\\Mbstring\\'
=> array($vendorDir . '/symfony/polyfill-mbstring'),
...
)

PSR4标准的初始化接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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 name
space separator."
);
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $len
gth;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}

PSR4初始化接口也很简单。如果没有顶级命名空间,就直接保存目录。如果有命名空间的话,要保证顶级命名空间最后是 \ ,然后分别保存( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )( 顶级命名空间 -> 目录 )这两个映射数组。具体形式可以查看上面我们讲的autoload_static的prefixLengthsPsr4、$prefixDirsPsr4 。
傻瓜式命名空间映射
autoload_classmap:

1
2
3
4
5
6
7
8
<?php
public static $classMap = array (
'App\\Console\\Kernel'
=> __DIR__ . '/../..' . '/app/Console/Kernel.php',
'App\\Exceptions\\Handler'
=> __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
...
)

addClassMap:

1
2
3
4
5
6
7
8
9
10
<?php
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $clas
sMap);
} else {
$this->classMap = $classMap;
}
}

这个最简单,就是整个命名空间与目录之间的映射。

结语

我们回顾一下,这篇文章主要讲了:

1.框架如何启动composer自动加载;
2.composer自动加载分为5部分;

其实说是5部分,真正重要的就两部分——初始化与注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。

前言

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

Composer自动加载源码分析——注册

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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()函数:

1
2
3
4
5
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prep
end);
}

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

Composer自动加载源码分析——全局函数的自动加载

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

1
2
3
4
5
6
7
8
9
10
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit832e
a71bfb9a4128da8660baedaac82e::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire832ea71bfb9a4128da8660baedaac82e($fileIde
ntifier, $file);
}

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

静态初始化:

ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files:

1
2
3
4
5
6
7
public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/sy
mfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/sy
mfony/var-dumper/Resources/functions/dump.php',
...
);

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

普通初始化

autoload_files:

1
2
3
4
5
6
7
8
9
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/pol
yfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var
-dumper/Resources/functions/dump.php',
....
);

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

加载全局函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e{
public static function getLoader(){
...
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire832ea71bfb9a4128da8660baedaac82e($fileIde
ntifier, $file);
}
...
}
}
function composerRequire832ea71bfb9a4128da8660baedaac82e($fileId
entifier, $file)
{
if (empty(\$GLOBALS['__composer_autoload_files'][\$fileIdent
ifier])) {
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这样控制加载会更快。还有一个原因我猜测应该是require_once对相对路径的支持并不理想,所以composer尽量少用require_once。

Composer自动加载源码分析——运行

我们终于来到了核心的核心——composer自动加载的真相,命名空间如何通过composer转为对应目录文件的奥秘就在这一章。   前面说过,ClassLoader的register()函数将loadClass()函数注册到PHP的SPL函数堆栈中,每当PHP遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以loadClass()函数就是自动加载的关键 了。 loadClass():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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/50
731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative) {
return false;
PHP Composer-——-注册与运行源码分析
41
}
$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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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_SEP
ARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $lo
gicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIR
ECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATO
R) . $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_SEP
ARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $lo
gicalPathPsr0)) {
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数组,查到下面这个数组:

1
2
3
4
5
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\的目录映射
为:

1
2
3
4
5
6
7
8
9
'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common
/src',
1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src
',
2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblo
ck/src',
),

遍历这个映射数组,得到三个目录映射;查看“目录+文件分隔符//+substr($logicalPathPsr4, $length)”文件是否存在,存在即返回。这里就是’_DIR_/../phpdocumentor/reflection-common/src + /+substr(phpDocumentor/Reflection/example_e.php(hh),25)’如果失败,则利用fallbackDirsPsr4数组里面的目录继续判断是否存在文件,具体方法是“目录+文件分隔符//+$logicalPathPsr4”

PSR0标准加载

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

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

1
2
3
4
5
6
7
8
9
10
11
'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目录寻找该文件,如果找到返回绝对路径。

0%