简读composer自动加载源码(个人笔记向)
Composer的基本学习
个人笔记向,存在有问题的描述请多包涵
Composer 的类型
首先我们先运行指令
$ composer init
$ Package name (<vendor>/<name>) [admin/learncomposer]: zxyy/composerlearn
$ Description []: study composer
$ Author [JunYu <1016673080@qq.com>, n to skip]:
$ Minimum Stability []: stable
$ Package Type (e.g. library, project, metapackage, composer-plugin) []: library
$ License []: mit
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]? n
Would you like to define your dev dependencies (require-dev) interactively [yes]? n
第一行是初始化composer的指令。第二行是问你所写的composer项目名是什么 作者/项目名。第三行当然就是项目的简介描述。第四行就是作者信息,直接回车跳过。第五行就是最小兼容版本。第六行就是问你你的这个composer项目是作为第三方扩展还是项目还是meta包,还是插件。第七行是授权模式。之后的就是问你是否需要第三方的依赖。我这边都写了N。
然后就得到了这么个文件
composer.json
{
"name": "zxyy/composerlearn",
"description": "study composer",
"type": "library",
"license": "mit",
"authors": [
{
"name": "JunYu",
"email": "1016673080@qq.com"
}
],
"minimum-stability": "stable",
"require": {}
}
关于这里的type
我主要说一下这个library和project的区别。前者的话所有的文件数据都会在vendor里面目录结构为vendor/admin/names 后者则在一级目录下。
composer自动加载
那么说到composer那首先想到的就是自动加载
自动加载有classmap
,psr-4
,psr-0
这几个,还有一个file
我们先举一个classmap的例子
composer.json
{
"name": "zxyy/composerlearn",
"description": "study composer",
"type": "library",
"license": "mit",
"autoload": {
"classmap": [
"Test/ClassMap"
]
},
"authors": [
{
"name": "JunYu",
"email": "1016673080@qq.com"
}
],
"minimum-stability": "stable",
"require": {}
}
然后在目录下创建一个Test/ClassMap文件夹且有一个Job类
Test/ClassMap/Job.php
<?php
namespace Test\ClassMap;
class Job
{
}
然后我们去终端执行
$ composer dump-autoload
然后我们再去看
vendor/composer/autoload_static.php
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e
{
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Test\\ClassMap\\Job' => __DIR__ . '/../..' . '/Test/ClassMap/Job.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->classMap = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$classMap;
}, null, ClassLoader::class);
}
}
我们可以发现在名为ComposerStaticInit的类文件中出现了一条
'Test\\ClassMap\\Job' => __DIR__ . '/../..' . '/Test/ClassMap/Job.php',
的记录,这条记录就是获取到了Job文件的绝对路径。
熟悉Laravel的同学们肯定是对file和psr-4有所了解。
在laravel中有一个叫App的命名空间,那么我们自己写要怎么去写呢?
composer.json
{
"name": "zxyy/composerlearn",
"description": "study composer",
"type": "library",
"license": "mit",
"autoload": {
"classmap": [
"Test/ClassMap"
],
"psr-4": {
"App\\":"app/"
}
},
"authors": [
{
"name": "JunYu",
"email": "1016673080@qq.com"
}
],
"minimum-stability": "stable",
"require": {}
}
在psr-4里面,我们前面所写的是你所取的命名空间名:
对应的则是实际目录的位置
所以我们现在就可以在文件目录中创建app\Psr4\Job.php
<?php
namespace App\Psr4;
class Job
{
}
然后我们再去执行一下
$ composer dump-autoload
看一下
vendor/composer/autoload_static.php
...
public static $prefixLengthsPsr4 = array (
'A' =>
array (
'App\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/app',
),
);
...
我们可以看到prefixlengthsPsr4和prefixDirsPsr4这俩个兄弟
前者是命名空间的长度,以及首字母开头
后者是命名空间所对应的文件夹的绝对路径
当然也可以做到一个命名空间对应多个文件夹
composer.json
...
"App\\":["app/","app1/"]
...
然后我们再去执行一下
$ composer dump-autoload
看一下
vendor/composer/autoload_static.php
...
public static $prefixDirsPsr4 = array (
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/app',
1 => __DIR__ . '/../..' . '/app1',
),
);
...
然后我们可以发现这里多一个app1的绝对路径
psr-4还支持无命名空间指定具体写法如下
"psr-4": {
"App\\":["app/","app1/"],
"":"nullspace/"
}
……
public static $fallbackDirsPsr4 = array (
0 => __DIR__ . '/../..' . '/NullSpace',
);
这个意思是说NullSpace目录下的所有不带命名空间的文件都通过psr-4加载
那么psr-0其实在使用上就和psr-4是一样的在此我就不多赘述。
我们来看最后一个就是files加载
使用laravel的朋友应该知道的,helps助手函数就是通过file加载在全局的。
composer.json
...
"files": [
"helpers.php",
"app/hello.php"
]
...
然后我们再去执行一下
$ composer dump-autoload
看一下
vendor/composer/autoload_static.php
public static $files = array (
'cf234aa6b2b7e8258c522027a10f3d31' => __DIR__ . '/../..' . '/helpers.php',
'4c89b7b03f917434285aa13e4af37b9f' => __DIR__ . '/../..' . '/app/hello.php',
);
我们就可以发现我们获取到了他们所处的绝对路径
Composer的加载过程
首先我们可以看到vendor\autoload.php
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e::getLoader();
我们可以发现先引入了/composer/autoload_real.php
返回了执行ComposerAutoloaderInit::getLoader()
的结果。
接下来的文字将用...
省略与我讲解无关的代码内容
我们打开/composer/autoload_real.php
文件
class ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
// 如果你是第一次执行就不会进入这个地方的判断
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'));
...
...
}
好我们先讲解以上的代码。
getLoader这个静态函数,首先判断,你是不是第一次执行,如果不是的话,就把已有的$loader也就是装载信息返回给你。
之后我们可以看到spl_autoload_register
这个函数,函数的第一个传参是一个数组[要运行的类名
,要运行的方法
],第二个传参是是否抛出错误,第三个是添加进autoload队列队首。
案例如下:
class A
{
public static function demo()
{
echo '我是'.__ClASS__;
require_once 'dede.php';
}
}
class B
{
public static function demo()
{
echo '我是'.__ClASS__;
}
}
class C
{
public static function demo()
{
echo '我是'.__ClASS__;
}
}
spl_autoload_register(['A','demo'],false,true);
spl_autoload_register(['B','demo'],false,true);
spl_autoload_register(['C','demo'],false,true);
// 当我们实例化一个PHP无法找到的类时,PHP就会去执行autoload列表
// 当然如果执行了以后还是没有这个类那就会理所当然的报错。
new dede();
当我们实例化一个PHP无法找到的类时,PHP就会去执行autoload列表。所以上面的案例执行后的结果如下
我是C我是B我是A
因为执行了spl三行以后 在autoload队列中是 C-B-A
然后new dede的时候发现当前文件中不存在,那就去执行了autoloader队列,然后从输出了我是C我是B我是A 输出我是A以后require了dede.php dede.php里面有dede这个类,所以实例化成功了。
还有一点要说明的是,实力化哪个类的时候出现错误,去触发了autoload列表时,我们可以在函数中用变量获取到类名(可以加上命名空间)
class C
{
public static function demo($class)
{
echo '触发的类名是'.$class;
// echo '我是'.__ClASS__;
}
}
spl_autoload_register(['C','demo'],false,true);
// 当我们实例化一个PHP无法找到的类时,PHP就会去执行autoload列表
new \Test\dede();
运行结果
触发的类名是Test\dede我是C
当然这样子还是有错误信息的,因为你没这个类嘛。
写一个简单的按需加载demo:
start.php
<?php
class start
{
public static function demo($class)
{
require_once __DIR__.'/'.$class.'.php';
}
public static function getLoader()
{
spl_autoload_register(['start','demo'],true,true);
}
}
start::getLoader();
$test = new momo();
momo.php
<?php
class momo
{
public function __construct()
{
echo '123';
}
}
运行start结果:
123
诶其实最简单的自动加载也就是这样子。
好接下来我们继续去读composer的代码
spl_autoload_register(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'));
如果接下来出现实例化,未找到的类就去执行composerAutoloaderInit::loadClassLoader
这边$loader
变量在实例化的时候并没有找到,所以就执行了spl_autoload_register
然后我们看loadClassLoader
public static function loadClassLoader($class)
{
var_dump($class);
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
我们可以去看输出的结果
"Composer\Autoload\ClassLoader"
如果此时为真,那么就引入ClassLoader.php
那么这个时候我们的
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
就可以执行成功了,实例化了类加载器,顺便传入了文件路径。
之后便是把注册器取消注册spl_autoload_unregister
我们继续往下看
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
这句代码的意思就是,现在使用的PHP版本是否大于5.6 是否使用了HHVM虚拟机(Facebook写的可以自己去百度) 和有没有使用zend加密PHP
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::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);
}
}
我这边就看第一个分支(我已经不用5.6 也没有用hhvm和zend)
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::getInitializer($loader));
call_user_func()可以执行回调,这个就不用我展开解释了吧?emm 说句不太正经的就是你return了一个function call_user_func帮你把它执行了~
我们打开这个autoload_static.php
重点看这个getInitializer
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$fallbackDirsPsr4;
$loader->classMap = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$classMap;
}, null, ClassLoader::class);
}
我就简单说一下这个Closure::bind
上案例了:
Class A
{
private static $name = '我是A';
public function getName(){
return self::$name;
}
}
Class B
{
private static $name = '我是B';
public function getName(){
return self::$name;
}
}
$closure = Closure::bind(function (){
echo $this->getName();
},new A(),null);
$closure();
输出
我是A
进程已结束,退出代码为 0
我们可以发现此时输出的是A
因为我们在第二个形参处丢了个实例化A的对象进去。
你如果要在这个闭包中用到$this关键字你就必须要在第二个形参处传入对象,否则就填写null 不使用
$closure = Closure::bind(function (){
echo $this->getName();
},null,null);
这样子就会报错
Fatal error: Uncaught Error: Using $this when not in object context in
那么第三个是什么呢?
$aa = new A();
$closure = Closure::bind(function () use ($aa){
echo $aa::$name;
},null,A::class);
$closure();
输出
我是A
进程已结束,退出代码为 0
$aa = new A();
$closure = Closure::bind(function () use ($aa){
echo $aa::$name;
},null,null);
$closure();
输出
Fatal error: Uncaught Error: Cannot access private property A::$name
由此我们可以得出第三个其实是newScope 就是一个作用域,我们填入以后我们就可以在这个闭包内使用到$aa 里面这个对象里面的私有成员。
好了接下来我们继续去看getInitializer
其实这个闭包就是将autoload_static.php
里面所记录的文件的所有绝对路径,赋值给autoload_real.php
里面的$loader实际上这个loader就是ClassLoader.php
里面的类。
然后代码就运行到
$loader->register(true);
这个register
其实就是classloader.php
中的。
好!接下来我们打开对应的部分。
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
这里我们可以看到$this::loadClass()
这么一回事,我们直接去看一下。
public function loadClass($class)
{
var_dump($class);
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
return null;
}
我们还可以发现接收了一个$class
那么这个class就是那个需要载入的类了。
我们也可以从findFile
这个函数可以发现一定有一个寻找文件的步骤。
然后文件寻找到以后,includeFile
就是引入文件了呀。
我们新建一个index.php 然后new一个之前使用classmap自动加载的类。
index.php
<?php
require_once "vendor/autoload.php";
new \Test\ClassMap\Job();
输出的内容为
string(17) "Test\ClassMap\Job"
接下来我们去看findFile
的部分
public function findFile($class)
{
// class map lookup
//$this->classMap 其实就是 autoload_static.php public static $classMap
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
//$this->classMapAuthoritative 布尔属性 如果True 或者 属于丢失类中就直接不加载
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
// apcu是PHP的一个自带缓存功能,判断你的文件路径是不是在缓存里面。
// 详情可见https://www.php.net/manual/en/book.apcu.php
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
// 到这里其实就是psr-4 和 psr-0这些方法的加载了
$file = $this->findFileWithExtension($class, '.php');
// 如果你用了hhvm虚拟机那就执行下面这个判断,组成的文件后缀为.hh
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
// 如果apcu缓存里木得 那就加进去
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
几乎每句代码我都注释了一下。
当classmap中不存在之后,composer就会为我们去psr-4或0也就是你设置的别的自动加载模式里面找。
$file = $this->findFileWithExtension($class, '.php');
接下来我们来看一下这个findFilewithExtension
此时我的index.php
为
<?php
require_once "vendor/autoload.php";
new \App\Psr4\Job();
这边只读psr-4
因为psr-0
其实流程上大家自己看也差不多的。
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
}
$logicalPathPsr4
其实就是更具psr-4
规则拼装好的一个文件路径
接下来我们去看autoload_static.php
中生成的数据 再结合这个部分的代码我相信就可以理解了
public static $prefixLengthsPsr4 = array (
'A' =>
array (
'App\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/app',
1 => __DIR__ . '/../..' . '/app1',
),
);
我来逐句解释其含义
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
首先将斜线改为相对系统所对应的分割线也就是DIRECTORY_SEPARATOR
的作用
我此时的$class
为App\Psr4\Job
然后取出首字母,也就是我这边的A
if (isset($this->prefixLengthsPsr4[$first])){
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
var_dump($lastPos);// 第一次下标为8
$subPath = substr($subPath, 0, $lastPos);
var_dump(substr($subPath, 0, $lastPos));
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
var_dump($pathEnd);
var_dump($logicalPathPsr4);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
判断是否存在$prefixLengthsPsr4[A]
这个元素
存在的话将其设定为$subPath
并且计算出最后一个反斜线出现的下标—–借助strrpos
函数
开始循环while第一次得出下标为8,借助substr提取出文件目录为App\Psr4
然后加上反斜线成为App\Psr4\
去prefixDirsPsr4
对比结果发现不存在,那么开始第二次循环。$search
成为了App\
成功进入判断$pathEnd='\Psr4\Job.php'
进入foreach
去组合绝对路径去判断文件是否存在,存在就返回路径。
最后执行到includeFile()
而includeFile?也就是如下:
function includeFile($file)
{
include $file;
}
一般的加载流程大概就是这样子了。
本作品采用《CC 协议》,转载必须注明作者和本文链接