Skip to content
// 0x
Go back
0x11 // 工具设计

「未实现」还是「不实现」:PPS 脚手架的设计选择

起因

在一次包生态维护中,我让 AI 全面评估 PPS(PHP Project Scaffold)工具。AI 交出的一份报告列出了五个”严重缺陷”:

每一个都是事实,但每一个都是故意的。当我说”这些都是设计选择”时,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.jsonphpunit.xml.distphpstan.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。

经验


Share this post on:

Previous Post
RAG + 本地/在线 LLM:一个可离线的 AI 客服架构
Next Post
全量 Vendor Rebrand:9 个 PHP 包从 hizpark 到 changhorizon 的实践记录