起因
在一次包生态维护中,我让 AI 全面评估 PPS(PHP Project Scaffold)工具。AI 交出的一份报告列出了五个”严重缺陷”:
- 占位符替换未实现
- src/tests 目录不存在
- 构造函数做 I/O
- MODE 环境变量在构造函数读取,测试无法切换
- cleanup() 在 finally 中重复调用
每一个都是事实,但每一个都是故意的。当我说”这些都是设计选择”时,AI 才意识到它用应用代码的标准评判了一个 CLI 工具。
这些东西我从来没写过文档——它们一直在我脑子里。这不怪 AI,怪我没有把”为什么这么做”写下来。
本文记录 PPS 的五个设计决策背后的实际理由。
PPS 是什么
PPS 的核心逻辑在一个 execute() 方法里:
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->checkoutTemplateFiles($template); // 复制模板到临时目录
$this->ensureProjectDirEmpty($force); // 检查目标目录
$this->deploy(); // 压缩 + 解压到目标
$this->cleanup(); // 清理临时文件
}
没有模板引擎,没有变量替换。它的职责只有一件事:确保从模板到目标目录的复制过程是完整且未被篡改的。
五个设计选择
1. 占位符替换不做——让用户经过每个配置
.pps.placeholders.php 文件定义了 16 个占位符。很多人第一次看到时会觉得”功能没做完”。但替换逻辑是我有意不写的。
理由很简单:我希望用户在初始化项目后,手动执行 grep 'pps\.' -r .,然后逐个 sed 替换。这个过程迫使用户看到并理解每一个配置——vendor name 填什么、namespace 怎么写、author email 对不对。不是”自动化不够”,而是手动替换本身就是流程的一部分。
许多年后我可能会改变主意,但至少目前,每用 PPS 生成一个新包时手动走一遍这套流程,让我对每个包的元数据有印象。
2. src/tests 目录不创建——脚手架只管配置
PPS 生成的是项目骨架的配置层——composer.json、phpunit.xml.dist、phpstan.neon.dist、.github/workflows/ci.yml。代码是用户的事。
如果创建了空的 src/ 和 tests/ 目录,git 要么跟踪空目录(需要加 .gitkeep),要么用户下次记得 mkdir。我选择把是否创建代码目录的决定权交给用户,而不是替用户做决定然后塞一个 .gitkeep 在那。
3. 构造函数做 I/O——CLI 工具不玩 DI
prepareTmpWorkDir() 在构造函数创建临时目录。从依赖注入的角度这是反模式——但 PPS 是一个 CLI 工具,它的生命周期是”启动 → 执行 → 退出”,没有服务容器,没有上下文切换。
放在构造函数的理由是 fail-fast:如果系统临时目录不可写,或者进程没有权限创建文件,那在最开始就失败,而不是在执行到一半时崩溃。
构造函数 → 检查 temp dir → fail-fast
execute() → 执行逻辑 → 正常完成
测试确实麻烦了一点,但测试本就不应该 mock temp 目录。
4. MODE 在构造函数读取——运行时不会变
PPS 有两种模式:MODE=local(独立 CLI 工具,在当前目录下创建项目)和 MODE=remote(用在 composer create-project 场景)。模式在进程启动时就确定了,不会在运行时切换。
放在构造函数读 getenv('MODE') 只是早定值。如果放在 execute() 里读,逻辑上一样,但语义上——模式是实例的属性,不属于一次执行。
5. cleanup() 被调用两次——finally 保障
cleanup() 在 try 块末尾和在 finally 块中各调用一次。Filesystem::remove() 对已删除路径是 no-op,所以不影响正确性。
理由很简单:如果有一天有人修改了 try 块提前 return,或者 execute() 中间的代码抛异常,finally 中的 cleanup() 保证临时文件一定被清理。这是一层防御性编程。
工具 vs 流程
回头看,AI 的第一次评估之所以列出”缺陷”,是因为它把 PPS 当工具评估——工具应该自动化一切。但 PPS 本质上是一个流程模板的发放器,它放出的不是”即用代码”,而是一个”等你填充的骨架”。
区别在于:工具追求自动化,流程追求可重复。
手动替换占位符不是”效率低”,而是流程中的确认环节。你不经过这一步,就不会注意到 composer.json 里还留着旧的 vendor name。
经验
- 评估任何工具前,先搞清楚它的设计范畴——CLI 工具和 Web 应用不应该用同一套标准评判
- 如果设计选择有理由但没写下来,就不能怪别人用自己的框架去理解
- 有时候”不做”比”做”更难坚持,因为每次都要解释为什么不做