composer听说过好久了,但是一直都没细节的去了解,简单查阅资料大部分是讲解命令,比如:composer install、composer update作用。autoload怎么实现的,暂时没有找到特别详细的。
先来了解下autoload。
首先,autoload有psr-0和psr-4两种规范。
psr-0:
psr-4:
其实最早找到的是英文版,不过甭管是中文还是英文,说实话刚看理解不了到底是啥意思。psr-4相对来说更好明白一点,psr-0有点绕不明白,虽然说是个被抛弃的规范,由于composer还是兼容psr-0,还是得学习下。继续看文字有点不懂,干脆就直接看源码,观察其实现autoload的原理。
先执行命令:composer require monolog/monolog
monolog库是初步学习的常用例子,库使用和结果反馈都非常简单清晰。这里作用是为了生成composer目录
核心目录如下所示:
monolog当前版本为:1.23.0,基于psr-4规范实现自动加载,不足以了解其他规范,因此,在composer.json内修改:
执行composer install
这部分内容是为了加载自定义的类,classmap和files又是什么呢?看了源码后就明白了,我们可以通过四种方式实现加载,这里先在项目多创建几个目录和php文件
开始关注和代码相关的内容~
require 'vendor/autoload.php';
composer的初步使用,就是在项目的入口文件require该指定文件,
调用autoload_real.php内的getLoader()静态方法。
spl_autoload_register(array('ComposerAutoloaderInit9dd8e0b0864cf05cbb09cc2bda0092ef', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit9dd8e0b0864cf05cbb09cc2bda0092ef', 'loadClassLoader'));
先注册类内loadClassLoader()静态方法autoload,实例同级目录下的ClassLoader对象,撤销注册方法loadClassLoader()
这块代码功能是实例化autoload的核心类ClassLoader。
//1、PHP版本大于等于5.6.0
//2、非HHVM运行环境
//3、不存在zend_loader_file_encoded方法
//4、若存在zend_loader_file_encoded方法,且执行结果为false
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
$useStaticLoader变量产生的分支,仅在与加载文件的路径是否存在../部分,若为true,基于当前目录进行定位,通过../向上级目录查找;若为false,以项目根目录进行定位向下级目录查找。
false:
//引入psr-0目录关系
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
//引入psr-4目录关系
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
//引入classmap目录关系
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
目前来说false几率很低,而且本质上也一样,所以这部分细节不展开。
true:
//导入autoload_static.php 文件。里面包含了psr-0、psr-4、classmap、files四种映射
require_once __DIR__ . '/autoload_static.php';
class ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef
{
public static $files = array (
'fa210f1e33fcb32837b5896ada32b141' => __DIR__ . '/../..' . '/common/Utils.php',
);
public static $prefixLengthsPsr4 = array (
'o' =>
array (
'one\\name\\' => 9,
),
'P' =>
array (
'Psr\\Log\\' => 8,
),
'M' =>
array (
'Monolog\\' => 8,
),
);
public static $prefixDirsPsr4 = array (
'one\\name\\' =>
array (
0 => __DIR__ . '/..' . '/src',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
'Monolog\\' =>
array (
0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
),
);
public static $prefixesPsr0 = array (
't' =>
array (
'two\\name\\' =>
array (
0 => __DIR__ . '/..' . '/src',
),
),
);
public static $classMap = array (
'Person' => __DIR__ . '/../..' . '/src/Person.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$prefixesPsr0;
$loader->classMap = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$classMap;
}, null, ClassLoader::class);
}
}
先来看看类的详细内容,
五个静态属性:$files、$prefixLengthsPsr4、$prefixDirsPsr4、$prefixesPsr0、$classMap
一个静态方法:getInitializer()
$files对应./common目录下的所有文件
$prefixLengthsPsr4、$prefixDirsPsr4对应psr-4
$prefixesPsr0对应psr-0
$classMap对应./src/Person.php
getInitializer()返回了一个Closure对象
这块我觉得特别有意思,专门了解了下闭包函数和Closure::bind的作用,从想要达到的效果来看,把autoload_static.php内的静态属性,赋值到ClassLoader实例的私有属性。
回到autoload_real.php,
//执行getInitializer()静态方法,返回一个闭包函数,再执行
//作用是将autoload_static.php 内的四种映射 复制到 ClassLoader类内
//private变量无法直接赋值,使用Closure::bind()方法,改变类作用域,这个就是闭包函数的作用
//通过call_user_func()函数执行闭包函数,对映射关系进行处理
call_user_func(\Composer\Autoload\ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::getInitializer($loader));
$loader->register(true);
=>
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
注册一个autoload调用方法,这就是后续最最关键的自动加载入口。
有没有发现上面的两种方式,都只引用了classmap、psr-0、psr-4,files都没操作过,实现过程就在最后一段逻辑里。
$files数组的键是一种不知名密文,对应的值为文件路径(包含文件名),直接使用了一个for循环,逐个require,同时设置了$GLOBAL变量,以键作为新键,标记$files内的文件是否载入。
也就是说,定义在$files里的文件,会在初始化阶段全部require,另外三种类似请求-再响应加载的方式。
再来看看ClassLoad内的加载逻辑方法:findFile($class)
// class map lookup
if (isset($this->classMap[$class])) {
//'Person' => __DIR__ . '/../..' . '/src/Person.php'
//classMap可能的格式如上,比如查找Person类,会判断是否包含,若存在则直接返回文件路径(包含文件名)
return $this->classMap[$class];
}
//判断apcu是否有使用
if (null !== $this->apcuPrefix) {
//获取缓存数据
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
//成功获取路径名
if ($hit) {
return $file;
}
}
//查找类文件 后缀为.php
$file = $this->findFileWithExtension($class, '.php');
//TODO 后面展开
//前面未找到apcu缓存且apcu已启动,则写入对应文件路径名
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
//文件加载失败,设置missingClasses全局变量,表示文件不存在
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
//返回加载结果
return $file;
入口只用判断返回的数据类型,若为false,则未查找成功;否则进行include操作。这里也有点小讲究:
根据psr自动加载的规范,自动加载器(autoloader)的实现一定不可抛出异常、一定不可触发任一级别的错误信息以及不应该有返回值,所以使用include,保证不会报错。
findFileWithExtension()内实现了psr-0和psr-4具体的加载逻辑,源码逻辑上注释我觉得是很好的学习方式;
private function findFileWithExtension($class, $ext){
// PSR-4 lookup
//将\替换成 系统分隔符
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
//例:class = one\name\Person
$first = $class[0]; //first => o
/*
$prefixLengthsPsr4 = array (
'o' =>
array (
'one\\name\\' => 9
)
);
* */
if (isset($this->prefixLengthsPsr4[$first])) { //查找键名为o的数组
//1、传入字符串 判断是否存在\字符
//2、舍去最后一个\后面及\的内容
//3、重新在后面添加一个\
//4、判断prefixDirsPsr4是否存在3生成的字符串键
//5、prefixDirsPsr4对应的值加上分隔符+类名,判断是否存在,存在则返回完整文件路径
//6、重要!保证查找前 文件路径的分隔符全部用 DIRECTORY_SEPARATOR 替换,做到不同系统的兼容性
$subPath = $class; //one\name\Person
//判断是受存在\字符,获取\最后一次出现的位置,下标
while (false !== $lastPos = strrpos($subPath, '\\')) {
//切割获取最后一个\前面部分内容,也就是去掉类名,完整的命名空间
$subPath = substr($subPath, 0, $lastPos); //one\name
$search = $subPath.'\\'; //one\name\
//通过prefixLengthsPsr4数组的处理,获取有效的命名空间信息
/*
$prefixDirsPsr4 = array (
'one\\name\\' =>
array (
0 => __DIR__ . '/..' . '/src'
)
);
*/
if (isset($this->prefixDirsPsr4[$search])) {
//拼接分隔符 + 类名 + 后缀 ==> /Person.php
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
//拼接映射路径 + 类名 + 后缀 ==> __DIR__ . '/..' . '/src' . '/Perso.php'
foreach ($this->prefixDirsPsr4[$search] as $dir) {
//判断文件是否存件
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
//$useStaticLoader === false fallbackDirsPsr4可能会被赋值
foreach ($this->fallbackDirsPsr4 as $dir) {
//查找fallbackDirsPsr4内的包含目录
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
//logicalPathPsr4 = two\\name\\Person.php
// namespaced class name
//logicalPathPsr0 = two\\name\\Person.php
//拼接好的字符串为:two\\name\\Person.php
logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
//将_替换成分隔符
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
/*
$prefixesPsr0 = array (
't' =>
array (
'two\\name\\' =>
array (
0 => __DIR__ . '/..' . '/src',
),
),
);
* */
if (isset($this->prefixesPsr0[$first])) { //判断是否存在t键名
//轮询t键对应的值
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
//prefix ==> two\\name\\
//dirs ==> 0
if (0 === strpos($class, $prefix)) {
//一个命令空间可以有多个目录,轮询查找是否存在该文件
foreach ($dirs as $dir) {
//dir ==> __DIR__.\\..\\src
//logicalPathPsr0 ==> two\\name\\Person.php
//这个和psr-4的区别很明显
//logicalPathPsr0 保留two\\name\\
//logicalPathPsr4 不保留one\\name\\
//判断 __DIR__.\\..\\src\ + two\\name\\Person.php 文件是否存在
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
//$useStaticLoader === false fallbackDirsPsr0可能会被赋值
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
总结来说
psr-4定义:"one\\name\\": "./vendor/src/"
psr-0定义:"two\\name\\": "./vendor/src/"
psr-4查找的路径为:./vendor/src/xx.php
psr-0查找的路径为:./vendor/src/two/name/xx.php
当然细节还有很多,这时候回去看看文档,也就看得懂了。
最后附上简单的运行代码和结果
require 'vendor/autoload.php';
//psr-0
$person0 = new \two\name\Person();
echo $person0->execute()."
";
//psr-4
$person4 = new \one\name\Person();
echo $person4->execute()."
";
//classmap
$person = new Person();
echo $person->execute()."
";
//files
echo Utils::execute();