基于 Entropy 框架的反射式依赖注入与签名即契约控制台开发指南
【免费下载链接】rectorInstant Upgrades and Automated Refactoring of any PHP 5.3+ code项目地址: https://gitcode.com/GitHub_Trending/re/rector
本文以仓库内 vendor/entropy/entropy/CLAUDE.md 为核心骨架,结合其 README.md 与
src/源码实现展开。Entropy 是一个要求 PHP 8.3+ 的极简框架(其 composer.json 中require为php: ^8.4),核心只有两块:基于反射的依赖注入容器(src/Container)与控制台运行器(src/Console)。它的最大特点是"零配置、零 YAML、零魔法字符串"——CommandInterface::run()的方法签名本身就是 CLI 契约。读完本文,你将掌握:如何用反射容器自动装配服务、如何用run()签名直接定义命令行参数与选项、@option注解的强制选项机制、数组参数的自动单数化约定,以及底层的映射与校验逻辑。
一、框架布局:先看清 Entropy 的目录结构
CLAUDE.md 用一份精炼的 Layout 概括了整个框架的组织方式:
src/Container— 自动装配容器(autowiring)、自动发现(autodiscovery)、契约查找(contract lookup)src/Console— 命令注册表、应用启动、输入输出、基于 docblock 的参数/选项映射src/Reflection— 反射辅助工具(参数解析、docblock@option标记解析器)src/FileSystem、src/Utils、src/Attributes— 配套支撑模块tests/— 与src/一一镜像,fixture 类位于各自的Fixture/子目录下
从源码目录树看,这一布局完全成立:src/Container下是Autodiscovery.php与Container.php两个核心类;src/Console下则细分出CommandRegistry.php、ConsoleApplication.php、Input/InputParser.php、Mapper/CLIRequestMapper.php、Mapper/CommandRunParametersMapper.php、Output/输出系列与ValueObject/值对象系列;src/Reflection下正是参数解析三件套ParameterDescriptionResolver、ParameterOptionMarkerResolver、ParameterTypesResolver等。
二、容器篇:反射驱动的依赖注入
2.1 自动发现:指向一个目录,全目录类皆服务
CLAUDE.md 的核心主张是"把容器指向一个目录,目录里每个类都自动成为可用服务"。README 给出了最小用法:
use Entropy\Container\Container; $container = new Container(); $container->autodiscover(__DIR__ . '/src'); $someService = $container->make(SomeService::class);从源码看,autodiscover()的实现在 src/Container/Container.php:
- 通过
Assert::directory($directory)校验目录存在; - 委托
Autodiscovery::autodiscoverDirectory()用FileFinder::findPhpFiles()扫描目录内所有 PHP 文件; - 逐个用
ClassNameResolver::resolveFromFilePath()从文件路径解析出类名; - 对已实例化(
instances)或已注册工厂(serviceFactories)的类跳过; - 为其余每个类注册一个懒加载工厂——真正
make()时才用ReflectionClass实例化。
Autodiscovery(src/Container/Autodiscovery.php)内部还会过滤掉不适合当服务的类:接口(isInterface())、异常(isSubclassOf(Throwable::class))、枚举(isEnum()),以及"没有父类也没有实现任何接口"的裸类。源码中还留有 TODO 注释,计划进一步排除命名空间含 ValueObject/DTO/Enum/Exception 的类。
2.2 手动注册工厂:需要自定义构造时
当默认的反射装配无法满足(例如需要外部资源、带参数的 PDO 连接)时,用service()注册工厂:
$container->service(PDO::class, function (Container $container): PDO { return new PDO('sqlite::memory:'); });注意service()的语义是"唯一注册":源码在 src/Container/Container.php 中,若同类已注册工厂会直接抛出RegisterServiceException,防止静默覆盖;同时一个工厂会取代同一类的裸注册。此外还有register()方法可登记"无工厂、按需反射构建"的类,且是幂等的。
2.3 契约查找:按接口拿回所有实现
需要某个接口的所有实现时,用findByContract():
$listeners = $container->findByContract(EventListenerInterface::class);底层逻辑(src/Container/Container.php):先通过warmUpInstanceServices()把已知属于该契约的工厂类与注册类全部make()预热进缓存,再array_filter出所有instanceof该接口的实例,最后array_values重新索引——这样返回的是纯 0 索引列表,可以直接展开给变参使用(如new Traverser(...$services)),不会因类名作为键而变成命名参数。构造函数中ParameterTypesResolver解析出array类型的参数时,容器正是调用findByContract()注入整个实现集合。
2.4 循环依赖检测:A -> B -> C -> A
容器在创建过程中维护making与makingStack两个结构(src/Container/Container.php):make()时先把类标记为"构建中"并压栈,若再次遇到正在构建的类,就用栈定位环的起点,拼出精确的循环链并抛出CreateServiceException,错误信息形如Circular dependency detected: A -> B -> C -> A。构建完成后,无论成败都会在finally中弹栈并解除标记,确保一次异常不会污染后续解析。
此外容器还提供afterResolving()回调(在实例构建完成后执行一次,可用于规避构造环的 setter 注入)与forgetByContract()(按契约遗忘工厂、注册与缓存实例)。值得注意,Container类被刻意设计为非 final 且可扩展(源码注释标注@api extendable container),供需要自定义解析策略的应用继承。
三、控制台篇:run() 签名即 CLI 契约
3.1 实现 CommandInterface,命令自动接线
控制台侧的核心约定是:一个命令的run()方法签名就是它的命令行定义。参数类型、默认值和 docblock 会自动翻译成 CLI 参数、选项与帮助文本——不需要任何 attribute,不需要手动接线输入对象。
use Entropy\Console\Contract\CommandInterface; use Entropy\Console\Enum\ExitCode; use Entropy\Console\Output\OutputPrinter; final readonly class HelloCommand implements CommandInterface { public function __construct( private OutputPrinter $outputPrinter ) { } public function getName(): string { return 'hello'; } public function getDescription(): string { return 'Say hello'; } /** * @param string[] $names Names to greet. * @param bool $loud Shout instead of speak. */ public function run(array $names, bool $loud = false): int { foreach ($names as $name) { $greeting = $loud ? "HELLO {$name}!" : "Hello {$name}"; $this->outputPrinter->green($greeting); } return ExitCode::SUCCESS; } }接口本身(src/Console/Contract/CommandInterface.php)极简:只需返回非空字符串的getName()与getDescription(),run()甚至没有在接口中声明签名(注释写明了"with many arguments"),完全由实现类自由定义。final readonly类、构造器注入OutputPrinter等写法与现代 PHP 风格完全兼容。
3.2 启动应用:三行代码完成引导
在入口二进制中引导整个应用:
use Entropy\Console\ConsoleApplication; use Entropy\Container\Container; $container = new Container(); $container->autodiscover(__DIR__ . '/src'); $consoleApplication = $container->make(ConsoleApplication::class); exit($consoleApplication->run($argv));容器构造函数里预设了一个内置工厂(src/Container/Container.php):CommandRegistry会通过findByContract(CommandInterface::class)自动收集所有命令——这就是"实现接口即自动接线"的机制来源。ConsoleApplication::run()(src/Console/ConsoleApplication.php)随后完成:解析argv→ 解析命令名 → 处理默认命令/帮助 → 映射参数 →$command->run(...$runArguments)展开调用,任何异常都会以红底输出并返回ExitCode::ERROR。
运行命令:
bin/console hello Alice Bob --loud3.3 参数/选项的映射规则
由CommandRunParametersMapper(src/Console/Mapper/CommandRunParametersMapper.php)实现签名到 CLI 定义的翻译,规则如下:
- 第一个
string/array参数按约定成为位置参数(positional argument);其余参数一律变成--option; - 复数数组选项名自动单数化:
$names参数会变成--name,$options变成--option(源码中用substr_compare(..., 's')判断末尾的s并去掉); - 驼峰参数名转 kebab-case 选项名:
dryRun→--dry-run(camelToKebab()的preg_replace('/[A-Z]/', '-$0', ...)逻辑); - 布尔参数是开关:
bool $loud = false默认false,出现--loud即为真; - 默认值参与映射:
[]这类"无意义"默认值会被归一化为null(源码 CommandRunParametersMapper.php); - 每个参数必须有显式类型声明,否则抛出
InvalidCommandException; - docblock 的
@param描述会进入帮助文本(ParameterDescriptionResolver负责解析)。
3.4 @option 强制标记:让首参也变成选项
按约定第一个 string/array 参数是位置参数,若希望它变成--option,在 docblock 中标注@option $name:
/** * @option $source * @param string $source The source path */ public function run(string $source, bool $verbose = false): int { // ... }bin/console hello --source=src/ParameterOptionMarkerResolver(src/Reflection/ParameterOptionMarkerResolver.php)负责解析:它读取run()的 docblock,逐行匹配/^@option\s+\$([A-Za-z_]\w*)\b/,收集所有被标记的参数名。CommandRunParametersMapper中,只要首参命中optionMarkers就会被归入选项而非参数;CLIRequestMapper同样据此保证被标记的参数绝不消费位置参数(src/Console/Mapper/CLIRequestMapper.php)。
3.5 类型强制转换与校验:CLI 字符串到类型化参数
CLIRequestMapper::castValueByParameterType()(src/Console/Mapper/CLIRequestMapper.php)把 CLI 上拿到的字符串按反射类型转换:
bool→filter_var($value, FILTER_VALIDATE_BOOLEAN)int→(int) $valuefloat→(float) $valuestring→(string) $valuearray→(array) $value- 标量类型收到数组时先
array_shift取单值;空值回落到参数默认值
映射的完整优先级(同文件resolveArguments())为:① 已出现的--option优先 → ②@option标记参数(无值时取默认/false,仍缺失则抛ConsoleInputMappingException)→ ③ 变参...$values消费全部剩余位置参数 → ④array类型参数把剩余位置参数收成单个数组 → ⑤ 单个位置参数 → ⑥ 默认值/布尔回退/必需值缺失报错。最后还会兜底校验:多余位置参数(提示改用array $values或变参收集)与未知选项(--help、--h、--version、--quiet等全局标志被显式忽略)都会报错。
3.6 输入解析:--opt=value、--opt value 与 -v 短标志
InputParser::parse()(src/Console/Input/InputParser.php)处理argv的全部形态:
- 长选项
--name=value用explode('=', $item, 2)拆分; - 长选项
--name value会把下一个非-开头的 token 当作值消费; - 裸长选项
--name值为true; - 短标志
-v记为$options['v'] = true; - 非数值的重复选项会累积成数组(支持多值选项);
- 其余 token 全部进入位置参数列表。
3.7 命令注册表:重复检测、模糊匹配与默认命令
CommandRegistry(src/Console/CommandRegistry.php)在构造时即校验:至少注册一个命令、命令名与描述非空、必须存在run()方法、命令名不得重复(否则抛InvalidCommandException)。命令名拼错时,FuzzyMatcher会给出最接近的匹配;实现DefaultCommandInterface的命令在无命令名时兜底执行(且未知的首个 token 会被当作它的第一个参数,如ecs src);实现HiddenCommandInterface的命令不会出现在帮助列表(getVisible())。
四、帮助系统与模糊匹配
CLAUDE.md 明确指出:传--help显示全局帮助,传<command> --help显示由 docblock 生成的逐命令帮助。
ConsoleApplication::run()中的分支逻辑(src/Console/ConsoleApplication.php)证实了这一点:无命令名且带--help/-h时打印全局帮助;CLIRequest::isCommandHelp()为真时,由CommandHelpFactory基于run()签名与@param描述构建该命令专属帮助文本,经HelpPrinter输出。也就是说,帮助文档完全由 docblock 驱动,写注释即写帮助,不存在需要单独维护的文档文件。
五、约定与工程质量:让框架保持极简的约束
CLAUDE.md 在 Conventions 一节总结的约定,正是保持框架零配置的纪律:
run()签名即 CLI 契约——第一个string/array参数是位置参数,其余是--option;必要时用@option $name强制改为选项;- 复数数组选项名自动单数化(
$names→--name); - 无 YAML、无装配 attribute、无魔法字符串——一切由容器自动发现,契约查找按接口收集实现;
- 测试使用 PHPUnit 12,
tests/**/Fixture/下的 fixture 类排除在 Rector 规则之外,避免自动重构误伤测试夹具。
CLAUDE.md 还给出了一套完整的开发命令(配合 composer.json 中的 require-dev 依赖):
vendor/bin/phpunit # 运行测试 vendor/bin/ecs # 编码规范检查 vendor/bin/ecs --fix # 自动修复编码规范 vendor/bin/rector # 应用 Rector 自动重构 vendor/bin/rector --dry-run # 预览 Rector 变更(不落盘) vendor/bin/phpstan # 静态分析 vendor/bin/composer-dependency-analyser # 未使用/遮蔽依赖分析(配置:composer-dependency-analyser.php)这套命令链覆盖了测试、编码规范(EasyCodingStandard)、自动重构(Rector)、静态分析(PHPStan)与依赖体检(ShipMonk 的 composer-dependency-analyser),与require-dev中的phpunit/phpunit: ^12.5、symplify/easy-coding-standard: ^13.1、phpstan/phpstan: ^2.1、rector/rector: ^2.4、shipmonk/composer-dependency-analyser: ^1.8等依赖一一对应,构成一个可直接复用的 PHP 包工程质量基线。
六、写在最后:这套设计的适用场景
Entropy 的设计哲学可以概括为:把"契约"全部收进类型签名与 docblock,让框架只做反射翻译。其价值在于:
- 对小型到中型 PHP 8.3+ 应用,容器自动发现 + 契约查找让服务注册代码趋近于零;
- 控制台命令零样板——写一个类、定义
run()签名与注释,CLI 参数、选项、帮助文本全部自动生成; - 循环依赖检测、未知参数/选项报错、模糊命令匹配等防御性设计,让错误在运行第一时间以清晰的异常信息暴露。
如果后续计划开发遵循"约定优于配置"的 PHP CLI 工具或微应用,src/Container与src/Console两套模块连同其tests/镜像结构,可作为最直接的参考实现;tests/**/Fixture/的目录划分方式与composer-dependency-analyser.php的依赖审计配置,也是值得借鉴的工程实践。
【免费下载链接】rectorInstant Upgrades and Automated Refactoring of any PHP 5.3+ code项目地址: https://gitcode.com/GitHub_Trending/re/rector
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考