前言
php 自5.3的版本之后,已经重焕新生,命名空间、性状(trait)、闭包、接口、psr 规范、以及 composer 的出现已经让 php 变成了一门现代化的脚本语言。php 的生态系统也一直在演进,而 composer 的出现更是彻底的改变了以往构建 php 应用的方式,我们可以根据 php 的应用需求混合搭配最合适的 php 组件。当然这也得益于 psr 规范的提出。
一、php 自动加载功能
php 自动加载功能的由来
在 php 开发过程中,如果希望从外部引入一个 class ,通常会使用 include
和 require
方法,去把定义这个 class 的文件包含进来。这个在小规模开发的时候,没什么大问题。但在大型的开发项目中,使用这种方式会带来一些隐含的问题:如果一个 php 文件需要使用很多其它类,那么就需要很多的 require/include
语句,这样有可能会 造成遗漏 或者 包含进不必要的类文件。如果大量的文件都需要使用其它的类,那么要保证每个文件都包含正确的类文件肯定是一个噩梦, 况且 require或 incloud 的性能代价很大。
php5 为这个问题提供了一个解决方案,这就是 类的自动加载(autoload)机制
。autoload机制
可以使得 php 程序有可能在使用类时才自动包含类文件,而不是一开始就将所有的类文件include
进来,这种机制也称为 lazy loading (惰性加载)
。
总结起来,自动加载功能带来了几处优点:
使用类之前无需 include / require
使用类的时候才会 include / require
文件,实现了 lazy loading
,避免了 include / require
多余文件。
无需考虑引入 类的实际磁盘地址 ,实现了逻辑和实体文件的分离。
php 自动加载函数 __autoload()
从 php5 开始,当我们在使用一个类时,如果发现这个类没有加载,就会自动运行 __autoload() 函数,这个函数是我们在程序中自定义的,在这个函数中我们可以加载需要使用的类。下面是个简单的示例:
<?php function __autoload($classname) { require_once ($classname . ".class.php"); }
在我们这个简单的例子中,我们直接将类名加上扩展名 .class.php
构成了类文件名,然后使用 require_once
将其加载。
- 从这个例子中,我们可以看出 __autoload 至少要做三件事情:
- 根据类名确定类文件名;
- 确定类文件所在的磁盘路径;
- 将类从磁盘文件中加载到系统中。
第三步最简单,只需要使用 include / require
即可。要实现第一步,第二步的功能,必须在开发时约定类名与磁盘文件的映射方法,只有这样我们才能根据类名找到它对应的磁盘文件。
- 当有大量的类文件要包含的时候,我们只要确定相应的规则,然后在
__autoload()
函数中,将类名与实际的磁盘文件对应起来,就可以实现lazy loading
的效果 。 - 如果想详细的了解关于 autoload 自动加载的过程,可以查看手册资料:php autoload函数说明
__autoload() 函数存在的问题
- 如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的, 其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须 在 __autoload() 函数中将所有的映射规则全部实现,这样的话
__autoload()
函数有可能会非常复杂,甚至无法实现。最后可能会导致__autoload()
函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。 - 那么问题出现在哪里呢?问题出现在 __autoload() 是全局函数只能定义一次 ,不够灵活,所以所有的类名与文件名对应的逻辑规则都要在一个函数里面实现,造成这个函数的臃肿。那么如何来解决这个问题呢?答案就是使用一个 __autoload调用堆栈 ,不同的映射关系写到不同的
__autoload函数
中去,然后统一注册统一管理,这个就是 php5 引入的spl autoload
。
spl autoload
- spl是 standard php library(标准php库)的缩写。它是 php5 引入的一个扩展标准库,包括 spl autoload 相关的函数以及各种数据结构和迭代器的接口或类。spl autoload 相关的函数具体可见
<?php // __autoload 函数 // // function __autoload($class) { // include 'classes/' . $class . '.class.php'; // } function my_autoloader($class) { include 'classes/' . $class . '.class.php'; } spl_autoload_register('my_autoloader'); // 定义的 autoload 函数在 class 里 // 静态方法 class myclass { public static function autoload($classname) { // ... } } spl_autoload_register(array('myclass', 'autoload')); // 非静态方法 class myclass { public function autoload($classname) { // ... } } $instance = new myclass(); spl_autoload_register(array($instance, 'autoload'));
spl_autoload_register() 就是我们上面所说的__autoload调用堆栈,我们可以向这个函数注册多个我们自己的 autoload() 函数,当 php 找不到类名时,php就会调用这个堆栈,然后去调用自定义的 autoload() 函数,实现自动加载功能。如果我们不向这个函数输入任何参数,那么就会默认注册 spl_autoload() 函数。
二、psr 规范
与自动加载相关的规范是 psr4,在说 psr4 之前先介绍一下 psr 标准。psr 标准的发明和推出组织是:php-fig,它的网站是:www.php-fig.org。由几位开源框架的开发者成立于 2009 年,从那开始也选取了很多其他成员进来,虽然不是 “官方” 组织,但也代表了社区中不小的一块。组织的目的在于:以最低程度的限制,来统一各个项目的编码规范,避免各家自行发展的风格阻碍了程序员开发的困扰,于是大伙发明和总结了 psr,psr 是 php standards recommendation 的缩写,截止到目前为止,总共有 14 套 psr 规范,其中有 7 套psr规范已通过表决并推出使用,分别是:
psr-0 自动加载标准(已废弃,一些旧的第三方库还有在使用)psr-1 基础编码标准
psr-2 编码风格向导
psr-3 日志接口
psr-4 自动加载的增强版,替换掉了 psr-0
psr-6 缓存接口规范
psr-7 http 消息接口规范
具体详细的规范标准可以查看php 标准规范
psr4 标准
2013 年底,php-fig 推出了第 5 个规范——psr-4。
psr-4 规范了如何指定文件路径从而自动加载类定义,同时规范了自动加载文件的位置。
1)一个完整的类名需具有以下结构:
\<命名空间>\<子命名空间>\<类名>
- 完整的类名必须要有一个顶级命名空间,被称为 "vendor namespace";
- 完整的类名可以有一个或多个子命名空间;
- 完整的类名必须有一个最终的类名;
- 完整的类名中任意一部分中的下滑线都是没有特殊含义的;
- 完整的类名可以由任意大小写字母组成;
- 所有类名都必须是大小写敏感的。
2)根据完整的类名载入相应的文件
- 完整的类名中,去掉最前面的命名空间分隔符,前面连续的一个或多个命名空间和子命名空间,作为「命名空间前缀」,其必须与至少一个「文件基目录」相对应;
- 紧接命名空间前缀后的子命名空间 必须 与相应的「文件基目录」相匹配,其中的命名空间分隔符将作为目录分隔符。
- 末尾的类名必须与对应的以 .php 为后缀的文件同名。
- 自动加载器(autoloader)的实现一定不可抛出异常、一定不可触发任一级别的错误信息以及不应该有返回值。
3 ) 例子
psr-4风格
类名:zendabc
命名空间前缀:zend
文件基目录:/usr/includes/zend/
文件路径:/usr/includes/zend/abc.php
类名:symfonycorerequest
命名空间前缀:symfonycore
文件基目录:./vendor/symfony/core/
文件路径:./vendor/symfony/core/request.php
目录结构
-vendor/ | -vendor_name/ | | -package_name/ | | | -src/ | | | | -classname.php # vendor_name\package_name\classname | | | -tests/ | | | | -classnametest.php # vendor_name\package_name\classnametest
三、composer自动加载过程
composer 做了哪些事情
- 你有一个项目依赖于若干个库。
- 其中一些库依赖于其他库。
- 你声明你所依赖的东西。
- composer 会找出哪个版本的包需要安装,并安装它们(将它们下载到你的项目中)。
例如,你正在创建一个项目,需要做一些单元测试。你决定使用 phpunit
。为了将它添加到你的项目中,你所需要做的就是在 composer.json
文件里描述项目的依赖关系。
{ "require": { "phpunit/phpunit":"~6.0", } }
然后在 composer require
之后我们只要在项目里面直接 use
phpunit 的类即可使用。
执行 composer require 时发生了什么
- composer 会找到符合 pr4 规范的第三方库的源
- 将其加载到 vendor 目录下
- 初始化顶级域名的映射并写入到指定的文件里
(如:'phpunit\\framework\\assert' => __dir__ . '/..' . '/phpunit/phpunit/src/framework/assert.php')
- 写好一个 autoload 函数,并且注册到 spl_autoload_register()里
题外话:现在很多框架都已经帮我们写好了顶级域名映射了,我们只需要在框架里面新建文件,在新建的文件中写好命名空间,就可以在任何地方 use 我们的命名空间了。
composer 源码分析
下面我们通过对源码的分析来看看 composer 是如何实现 psr4标准
的自动加载功能。
很多框架在初始化的时候都会引入 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 composerautoloaderinit7b790917ce8899df9af8ed53631a1c29::getloader();
这里就是 composer 真正开始的地方了
composer自动加载文件
首先,我们先大致了解一下composer自动加载所用到的源文件。
autoload_real.php: 自动加载功能的引导类。
- composer 加载类的初始化(顶级命名空间与文件路径映射初始化)和注册(spl_autoload_register())。
classloader.php : composer 加载类。
- composer 自动加载功能的核心类。
autoload_static.php : 顶级命名空间初始化类,
- 用于给核心类初始化顶级命名空间。
autoload_classmap.php : 自动加载的最简单形式,
- 有完整的命名空间和文件目录的映射;
autoload_files.php : 用于加载全局函数的文件,
- 存放各个全局函数所在的文件路径名;
autoload_namespaces.php : 符合 psr0 标准的自动加载文件,
- 存放着顶级命名空间与文件的映射;
autoload_psr4.php : 符合 psr4 标准的自动加载文件,
- 存放着顶级命名空间与文件的映射;
autoload_real 引导类
在 vendor 目录下的 autoload.php
文件中我们可以看出,程序主要调用了引导类的静态方法 getloader()
,我们接着看看这个函数。
<?php public static function getloader() { if (null !== self::$loader) { return self::$loader; } spl_autoload_register( array('composerautoloaderinit7b790917ce8899df9af8ed53631a1c29', 'loadclassloader'), true, true ); self::$loader = $loader = new \composer\autoload\classloader(); spl_autoload_unregister( array('composerautoloaderinit7b790917ce8899df9af8ed53631a1c29', 'loadclassloader') ); $usestaticloader = php_version_id >= 50600 && !defined('hhvm_version'); if ($usestaticloader) { require_once __dir__ . '/autoload_static.php'; call_user_func( \composer\autoload\composerstaticinit7b790917ce8899df9af8ed53631a1c29::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\composerstaticinit7b790917ce8899df9af8ed53631a1c29::$files; } else { $includefiles = require __dir__ . '/autoload_files.php'; } foreach ($includefiles as $fileidentifier => $file) { composerrequire7b790917ce8899df9af8ed53631a1c29($fileidentifier, $file); } return $loader; }
我把自动加载引导类分为 5 个部分。
第一部分——单例
第一部分很简单,就是个最经典的单例模式,自动加载类只能有一个。
<?php if (null !== self::$loader) { return self::$loader; }
第二部分——构造classloader核心类
第二部分 new 一个自动加载的核心类对象。
<?php /***********************获得自动加载核心类对象********************/ spl_autoload_register( array('composerautoloaderinit7b790917ce8899df9af8ed53631a1c29', '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) { require_once __dir__ . '/autoload_static.php'; call_user_func( \composer\autoload\composerstaticinit7b790917ce8899df9af8ed53631a1c29::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 静态初始化 ( php >= 5.6 )
静态初始化只支持 php5.6 以上版本并且不支持 hhvm 虚拟机。我们深入 autoload_static.php
这个文件发现这个文件定义了一个用于静态初始化的类,名字叫 composerstaticinit7b790917ce8899df9af8ed53631a1c29
,仍然为了避免冲突而加了 hash 值。这个类很简单:
<?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
,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。
classloader 接口初始化( php < 5.6 )
如果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
的 $prefixlengthspsr4 、 $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 的静态初始化到这里就完成了!
其实说是5部分,真正重要的就两部分——初始化与注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。
第四部分 —— 注册
讲完了 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); }
其实奥秘都在自动加载核心类 classloader 的 loadclass() 函数上:
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。
静态初始化:
composerstaticinit7b790917ce8899df9af8ed53631a1c29::$files:
public static $files = array ( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __dir__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '667aeda72477189d0494fecd327c3641' => __dir__ . '/..' . '/symfony/var-dumper/resources/functions/dump.php', ... );
普通初始化
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 composerautoloaderinit7b790917ce8899df9af8ed53631a1c29{ public static function getloader(){ ... foreach ($includefiles as $fileidentifier => $file) { composerrequire7b790917ce8899df9af8ed53631a1c29($fileidentifier, $file); } ... } } function composerrequire7b790917ce8899df9af8ed53631a1c29($fileidentifier, $file) { if (empty(\$globals['__composer_autoload_files'][\$fileidentifier])) { require $file; $globals['__composer_autoload_files'][$fileidentifier] = true; } }
第五部分 —— 运行
到这里,终于来到了核心的核心—— 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; } }
总结
我们通过举例来说下上面代码的流程:
如果我们在代码中写下 new phpdocumentor\reflection\element()
,php 会通过 spl_autoload_register 调用 loadclass -> findfile -> findfilewithextension。步骤如下:
- 将 \ 转为文件分隔符/,加上后缀php,变成 $logicalpathpsr4, 即 phpdocumentor/reflection//element.php;
- 利用命名空间第一个字母p作为前缀索引搜索 prefixlengthspsr4 数组,查到下面这个数组:
p' => array ( 'phpdocumentor\\reflection\\' => 25, 'phpdocumentor\\fake\\' => 19, )
- 遍历这个数组,得到两个顶层命名空间 phpdocumentor\reflection\ 和 phpdocumentor\fake\
- 在这个数组中查找 phpdocumentor\reflection\element,找出 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/element.php,25)'
- 如果失败,则利用 fallbackdirspsr4 数组里面的目录继续判断是否存在文件
以上就是php composer 的自动加载原理的详细内容,更多关于php composer 的自动加载的资料请关注代码网其它相关文章!
发表评论