拍片至今,参与的大多项目利用 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以内。到这里也就全部结束。
- 重写父级 run 方法,如果没有输出流就调用 Factory::createOutput() 创建,红色高亮警告为黄字黑底的终端样式,然后继续执行父级方法,携带输出流;当然,父级方法也是做了判断,不存在就创建,注意
到这里。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
,