拍片至今,参与的大多项目利用 Composer 管理各种依赖,有效提高了开发效率,也源于社区活跃程度,才出现了形式各样的依赖供我这种低级开发者使用。但从未去真正了解过它的实现,给它的定位就是工具,能用就行。现在看来,似乎行不大通。所以,还是需要抽空去了解具体实现。
下载
这里采用命令操作的,如果 Windows,直接从网页端 Download ZIP 也行,然后解压。至于是否重命名,看个人意愿。
wget https://github.com/composer/composer/archive/refs/heads/master.zip
tar -zxf master.zip && rm master.zip
mv composr-master composer && rm -rf composr-master
然后使用 Phpstorm 打开对应目录就行了。由于项目源码的原因,这里需要其他的依赖,所以,要在项目根目录执行 composer install 安装依赖。出了异常 Could not scan for classes inside "phpstan/Rules/tests/data" which does not appear to be a file nor a folder ,为此,移除了 composer.json 中的 autoload-dev 节点下的 classmap 设置。不对,可以加标记,比如 composer install --no-dev 忽略 require-dev 部分也可以。
分析
目录结构
采用无序列表表示。
- bin 有关 composer 使用到的命令脚本文件,方便测试;
- doc 文档,顾名思义;
- res 存放的模式;
- src 源码部分;
- vendor 其他依赖包,经
composer install 产生的。
bin 下的两个命令脚本
compile
将 composer 编译到一个独立文件(默认为 composer.phar)。由于这里会获取版本库最后一次提交时间(git log -n1 --pretty=%ct HEAD )作为后缀拼接,我这边是直接下载压缩包,不是 Git 仓库,所以,下载那里,估计还是得采用 Fork 模式做,当然也可以直接初始化一个仓库,顺便做个提交,似乎也行得通。我采用了第二种方式,但是 php.ini => phar.readonly 需要关闭,注意一下,否则会出错 Failed to compile phar: [UnexpectedValueException] creating archive "composer.phar" disabled by the php.ini setting phar.readonly 。速度还行,已经生成了 compser.phar 压缩文件。那先看看这个编译脚本吧。 先使用 require __DIR__.'/../src/bootstrap.php' 引入启动脚本保证 use Composer\Compiler 的可用性。该类实例化后直接调用编译方法 (new Compiler())->compile() ,为了表述,代码有一定压缩,当然做了捕获异常处理。接下来,进入这个编译类。 主要有三个私有属性,version 最后提交 Hash 码,branchAliasVersion 分支对应版本,在 composer.json 中配置指向 extra.branch-alias.dev-master 节点,versionDate 最后提交时间,一共有六个方法,其中五个为私有,限制比较严格,也就是说,只有编译那个方法可调用。
- Composer.getRelativeFilePath 获取文件相对路径;
对文件绝对路径采用两次 dirname,这里已知的,当前文件和项目根目录的距离,然后定位子串在原串位置,使用 substr_replace 替换,最后 strtr 替换特定字符。 - Compiler.addFile 向 phar 压缩文件中添加文件;
获取所要添加文件相对路径后,把文件流载入字符串,然后根据参数 $strip 去除空格,如果是证书文件就不用去,直接拼接。还有个特殊文件 src/Composer/Composer.php,需要替换一些字符串。最后调用 Phar.addFromString 加到压缩文件。 - Compiler.stripWhitespace 去掉空格;
- Compile.addComposerBin 添加 composer 文件;
- Compile.getStub 获取末节内容;
- Compile.compile 编译方法;
一来就是压缩文件存在即删除,毕竟要新建。然后就是,通过仓库获取最后提交信息和标签(如果没有标签,就解析 composer.json 中获取)。添加文件的顺序也很讲究啊,依次是:composer 源码(排除了 Compiler.php/ClassLoader.php/InstalledVersions.php)=> ClassLoader.php => InstalledVersions.php => 添加 vendor 下的文件 => 扩展文件 => bin/composer => 末节 => 证书。
所以,大概也了解了生成压缩包的大概流程,但对 composer 的过程,还是不清楚。估计是要看第二个命令脚本。
composer
这里就是正常流程。不过,在非 cli/phpdbg 环境下执行 php bin/composer 会输出警告级异常,不会强行中断。有一行看不懂 setlocale(LC_ALL, 'C') ,就挺尴尬,先放在这里,也问过朋友,说是设置地区信息,但是这个字符 C 。紧接着,就引入启动文件,做一些其他检测,内存限制和环境参数设置。最后还是 (new Composer\Console\Application())->run() 就完了。应该就是这个类,入口,我看到了 logo。
流程分析
应用主类 Application,位于 src/Composer/Console/Application.php,继承 Symfony 控制台应用的实现。
- Application.__construct():应用主类构造方法;
- 关闭 Xdebug 的输出干扰, xdebug.show_exception_trace 和 xdebug.scream;
- 注册 php 中止函数 register_shutdown_function;
- Application.run: 应用运行方法;
- 重写父级 run 方法,如果没有输出流就调用 Factory::createOutput() 创建,红色高亮警告为黄字黑底的终端样式,然后继续执行父级方法,携带输出流;当然,父级方法也是做了判断,不存在就创建,注意
$input 是终端参数类 ArgvInput 的实例化。调用 configureIO() 方法通过用户参数和选项设置 IO; - 切入 doRun() 方法。还是有重写;
- 一来就是检测插件标记
--no-plugins 设置属性 disablePluginsByDefault,然后检测界面是否可交互; - 然后将当前 IO 和 QuestionHelper(用户交互助手) 重新实例化一个终端 IO 助手 ConsoleIO,并注册到 将错误转化异常的助手 ErrorHandle;
- 检测参数
--no-cache ,如果为真,就给出提示,并把环境变量 COMPOSER_CACHE_DIR 定到 nul 或 /dev/null,主要根据系统选择的; - 切换工作目录,如果命令参数中有设置
-working-dir 。然后,获取当前命令名称,也就是参数的第一个,比如 composer install ,那么命令名称就是 install。当然,如果更换了工作目录,是要对当前目录下的 composer.json 文件存在性检测,没有的话,会给出相应提示; 继续检测插件命令,这里先留着 ;- 检测是否为代理命令。如果非代理模式,检测 PHP 版本,Xdebug 和超过60天没有更新,以及非 Windows 环境检测当前运行用户是否为根用户;
- 检测系统临时目录 sys_temp_dir,在 php.ini 配置文件中设置;
- 将当前 composer.json 文件中 scripts 节点下配置非标准命令加入到自身命令集合;
- 检测是否显示时间,通过参数
--profile ; - 调用父级 doRun()` 方法;
- 检测命令是否查看版本(
--version / -V ),如果是,直接输出; - 处理帮助命令;
- 如果没有命令,就设置为 list;
- 标记当前运行的命令,doRunCommand()` 执行完后,重置;
- 检测是否设置调度器,没有的话,直接调用命令的执行,这里就不跟进。看看有调度的情况;
- 如果存在调度的情况,将当前的 IO 和命令作为参数实例化 ConsoleCommandEvent。异步?
- 如果终端命令事件可以执行,就执行。如果不能,就将状态码设置禁用。出现了异常,就调用终端异常事件类处理,最后主动调用关闭 ConsoleTerminateEvent;
- 还原目录(如果设置了工作目录),打印时间(如果设置了
--profile ),恢复错误; - 异常处理,如果有,渲染并设置退出代码,这里如果自动退出,还要将代码控制在256以内。到这里也就全部结束。
到这里。composer 整体的主干流程也就结束了,接下来,该是对具体命令的分析,粗略看了一下,在 src/Composer/Command 目录中大概有三十多个命令脚本文件。也可以挨着看,真要算起来,也不多。
命令分析
这里仅分析 src/Composer/Command 目录下的命令脚本文件。需要注意,php bin/composer command args 是在 composer 源码项目中调试使用的,如果是在正式环境中,直接 composer command args 即可。这里我采用在 composer 源码项目中调试。
- about,文件 AboutCommand.php,参考命令
php bin/composer about ,查看 Composer 的简要描述; - archive,文件 ArchiveCommand.php,参考命令
php bin/composer archive ,创建一个发布包的压缩文件; - clear-cache,文件 ClearCacheCommand.php,参考命令
php bin/composer clear-cache ,清除缓存; - config,文件
ConfigCommand.php ,设置; - create-project,文件
CreateProjectCommand.php ,根据包创建项目; - depends,文件
DependsCommand.php ,参考命令 php bin/compose depends psr/log ,查看对此包有相关依赖关系的包; - diagnose,文件
DiagnoseCommand.php ,参考命令 php bin/composer diagnose ,诊断系统以识别常见错误; - dump-autoload,文件
DumpAutoloadCommand.php ,参考命令 php bin/composer dump-autoload ,配置自动加载文件; - exec,文件
ExecCommand.php ,参考命令 php bin/composer exec ,执行申明的二进制或脚本; - fund,文件
FundCommand.php ,参考命令 php bin/composer fund ,了解如何维护依赖; - global,文件
GlobalCommand.php ,参考命令 php bin/composer global command ,全局运行命令; - browse,文件
HomeCommand.php ,参考命令 php bin/composer browse psr/log ,在浏览器中打开这个依赖的仓库地址; - init,文件
InitCommand.php ,参考命令 php bin/composer init ,在当前目录创建 composer.json 文件初始化 composer 仓库; - install,文件
InstallCommand.php ,参考命令 php bin/composer install ,安装依赖; - licenses,文件
LicensesCommand.php ,参考命令 php bin/composer licenses ,查看所有依赖的证书; - outdated,文件
OutdatedCommand.php ,参考命令 php bin/composer outdated ,查看依赖是否有更新; - prohibits,文件
ProhibitsCommand.php ,参考命令 php bin/composer prohibits ,显示阻止安装的依赖; - reinstall,文件
ReinstallCommand.php ,参考命令 php bin/composer reinstall package ,重新安装指定依赖; - remove,文件
RemoveCommand.php ,参考命令 php bin/composer remove package ,移除指定依赖; - require,文件
RequireCommand.php ,参考命令 php bin/composer require package ,添加并安装依赖; - run-script,文件
RunScriptCommand.php ,参考命令 php bin/composer run-script script-name ,运行在 composer.json 中定义的脚本命令; - search,文件
SearchCommand.php ,参考命令 php bin/composer search package ,搜索依赖; - self-update,文件
SelfUpdateCommand.php ,参考命令 php bin/composer self-update ,更新 composer; - show,文件
ShowCommand.php ,参考命令 php bin/composer show package ,获取依赖相关信息; - status,文件
StatusCommand.php ,参考命令 php bin/composer status ,查看已更新的本地依赖; - suggests,文件
SuggestsCommand.php ,参考命令 php bin/composer suggests ,展示依赖建议; - update,文件
UpdateCommand.php ,参考命令 php bin/composer update ,更新依赖; - validate,文件
ValidateCommand.php ,参考命令 php bin/composer validate ,验证 composer.json 和 composer.lock 文件。
这里,主要考虑到命令较多,只是罗列命令和它的作用。至于,分析的话,后面,再抽个时间,补充几个常用命令的逻辑。
自动加载
在需要场景中,引入 vendor/autoload.php 文件即可,使用 require_once 引入 autoload_real.php 文件,调用 getLoader() 静态方法获取加载器,实际是在这里注册好的,采用 spl_autoload_register 函数实现五种加载方式。 临时注册一个文件自动加载,主要是为了引入 ClassLoader.php 文件,实例化后注销文件自动加载,说明对此类应该属于核心,大部分操作都在此类中。
静态加载
autoload_static.php,主要加载静态类(其中方法全为静态)(这里描述有问题,不过好像是这样的) ,需要同时满足三个条件:
- php 版本大于 5.6;
- 非 HHVM(由 Facebook 打造的 PHP 虚拟机)环境;
- 检测函数 zend_loader_file_encoded 是否存在,如果存在就是要可用的。也就是说,要不不存在,要不存在且可用,这两种为真;
如果满足,通过 require 引入 autoload_static.php,然后 call_user_func(\Composer\Autoload\ComposerStaticInitComposerPhar1625709410::getInitializer($loader)) ,将当前加载器作为参数传入,进行初始化设置。主要匿名闭包处理 Closure::bind 。
- 设置 PSR-4 类前缀长度
$loader->prefixLengthsPsr4 = ComposerStaticInitComposerPhar1625709410::$prefixLengthsPsr4 ; - 设置 PSR-4 类目录
$loader->prefixDirsPsr4 = ComposerStaticInitComposerPhar1625709410::$prefixDirsPsr4 ; - 设置 PSR-0 类目录
$loader->prefixesPsr0 = ComposerStaticInitComposerPhar1625709410::$prefixesPsr0 ; - 设置类映射
$loader->classMap = ComposerStaticInitComposerPhar1625709410::$classMap ;
看来,稍微不讲究,直接生成的静态属性,这里只管读取配置。不过这样挺好,因为生成少,读取多,直接固定,减少了寻找匹配的损耗。
命名空间加载
通过 require 引入 autoload_namespaces.php,使用 $load->set($namespace, $path) ,将类前缀和 PSR-0 关联起来,类似下面这种:
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'React\\Promise' => array($vendorDir . '/react/promise/src'),
);
PSR-4 标准加载
通过 require 引入 autoload_psr4.php,使用 $loader->setPsr4($namespace, $path) ,进行相应注册绑定。
类名映射加载
通过 require 引入 autoload_classmap.php,使用 $loader->addClassMap($classMap) ,进行相应注册绑定。
文件加载
如果是静态加载,直接读取静态属性获取数组,如果不是,那就通过 require 引入 autoload_files.php,并设置标记,如下:
function composerRequireComposerPhar1625709410($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
抛开文件加载,在静态加载和其他三加载,其实大体相似,不过,静态加载,拥有更多的信息,比如长度和首字母等,然后一次性注册。不过需要注意的是,在文件加载,会将当前实例注册为自动加载器并且预载入 spl_autoload_register(array($this, 'loadClass'), true, $prepend) ,这里 $this 是 ClassLoader.php 的实例。既然注册已经完成,那来看下具体加载方式吧。
具体加载 loadClass
如果找到了相应文件,直接 include 的,不过还是简单包装了一下。主要分析查找过程 findFile,由于 include 实现,所以这个方法需要返回类路径。
- 类映射,这个很方便,直接从属性 classMap 中检测当前类是否存在对应,如果有,直接返回即可,否则继续往下走。检测是否只允许这种方式的标记 classMapAuthoritative 或是否被动过滤的类(查找一次失败后,会被记录在缺少类(missingClasses)中),如果为真,返回 false。
- 检测 apcu 缓存,
$file = apcu_fetch($this->apcuPrefix.$class, $hit) ,这块也没怎么用过,操作码缓存,如果 $hit 为真,就返回
f
i
l
e
,
如
果
后
续
检
测
到
了
相
关
类
,
还
要
使
用
‘
a
p
c
u
a
d
d
(
file,如果后续检测到了相关类,还要使用 `apcu_add(
file,如果后续检测到了相关类,还要使用‘apcua?dd(this->apcuPrefix.$class, $file)` 进行补充; - 根据后缀检索文件,主要是 php 和 hh(HHVM环境),看来, PSR-0 和 PSR-4 以及文件加载都在这步,这里直接贴源码解释;
private function findFileWithExtension($class, $ext)
{
$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;
}
}
}
}
foreach ($this->fallbackDirsPsr4 as $dir) if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) return $file;
if (false !== $pos = strrpos($class, '\\')) {
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1).strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
$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;
}
}
}
}
foreach ($this->fallbackDirsPsr0 as $dir) if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) return $file;
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) return $file;
return false;
}
写在最后
差点就放弃了,不过中间有关命令的具体解析,还是略过,不太想引入大片大片源码。从自动加载器看,主要是对 include 进行了包装和优化,将依赖集中管理。对于项目来说,确实挺方便,就在 composer.json 文件中管理具体需要使用的依赖,采用 composer 命令进行维护。省略了细节和底层。而这种细节,确实一般也不是开发所关心的,他们大多着重业务逻辑的实现。至于,采用什么依赖,大多方便行事。
比如框架这种产物,是需要明确固定依赖,毕竟维护框架的生命周期和固化功能的有效性。不是必需的,一般还是看具体场景抉择。按需引入,符合 CRP 原则。这也是在保证固有功能的前提下,尽量轻量化。
|