Tagged Iterator 与优先级排序
解读
在国内中大型 PHP 面试中,这道题通常出现在「依赖注入 / 组件扩展」环节,考察候选人对 Symfony 组件(或 Laravel 服务容器)的深度理解。
面试官想确认三件事:
- 你是否知道「标签(tag)」机制,而不是只会按名称手动注入;
- 能否利用 TaggedIterator(或 Laravel 的 Tag)一次性收集所有同类服务;
- 当业务需要“先主后次”或“先缓存后DB”等顺序时,能否用优先级字段完成排序,而不是写死顺序。
答不出“优先级排序”会被判定为“只会用,不会设计”;答不出“编译期排序”会被追问性能,直接决定能否进入架构组。
知识点
- Symfony DI 标签(tag)
- 给服务打标签:tags: [{ name: app.handler, priority: 100 }]
- 编译期收集:TaggedIteratorArgument('app.handler')
- 优先级字段
- 默认 0,越大越靠前;负数合法
- 排序发生在 Pass 阶段,容器编译完成后顺序固定,运行时无额外开销
- 自定义 CompilerPass
- 实现 CompilerPassInterface,在 process() 内 $container->findTaggedServiceIds() 拿到全部标签,按 priority 排序后注入指定服务
- PHP 排序实现
- usort + 匿名函数,注意稳定性(PHP 8 稳定,PHP 7 需补稳定算法)
- Laravel 映射
- 容器无 TaggedIterator,但可用 $app->tagged('foo') 收集,再用 Collection::sortByDesc('priority') 完成同等效果
- 性能与可维护性
- 编译期排序保证 O(n log n) 只跑一次,生产环境仅顺序遍历,无反射、无 I/O
- 新增处理器只需加标签,零侵入原有代码,符合开闭原则
答案
以 Symfony 6 为例,演示“可排序的消息处理器”全过程,关键代码如下。
- 定义接口
interface MessageHandlerInterface
{
public function handle(Message $msg): void;
public static function getPriority(): int; // 优先级字段
}
- 写两个实现,优先级不同
final class SmsHandler implements MessageHandlerInterface
{
public function handle(Message $msg): void { /* */ }
public static function getPriority(): int { return 80; }
}
final class EmailHandler implements MessageHandlerInterface
{
public function handle(Message $msg): void { /* */ }
public static function getPriority(): int { return 100; }
}
- 注册服务并打标签
services:
_defaults:
autowire: true
autoconfigure: true
tags: [{ name: 'app.message_handler' }]
App\Handler\SmsHandler: ~
App\Handler\EmailHandler: ~
- 自定义 CompilerPass 完成排序注入
final class MessageHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(HandlerChain::class)) { return; }
$definition = $container->findDefinition(HandlerChain::class);
$tagged = $container->findTaggedServiceIds('app.message_handler');
$services = [];
foreach ($tagged as $id => $tags) {
$class = $container->getDefinition($id)->getClass();
$services[] = [
'id' => $id,
'priority' => $class::getPriority(),
];
}
// 按 priority 降序,稳定排序
usort($services, fn($a, $b) => $b['priority'] <=> $a['priority']);
$refs = array_map(fn($s) => new Reference($s['id']), $services);
$definition->setArgument(0, $refs);
}
}
- 调用端零感知顺序
final class HandlerChain
{
public function __construct(
/** @var MessageHandlerInterface[] */
private iterable $handlers
) {}
public function handle(Message $msg): void
{
foreach ($this->handlers as $handler) {
$handler->handle($msg);
}
}
}
容器编译后,HandlerChain 内 handlers 顺序已按 priority 排好,运行时无需再排序,符合国内高并发场景“编译期做重活、运行期做轻活”的规范。
拓展思考
- 如果优先级相同,如何保证“注册顺序”不被打乱?
PHP 8 的 usort 是稳定的;PHP 7 可在数组中追加原始索引,用 priority+index 二次键排序。 - 运行期动态改优先级是否可行?
不建议。会退回到反射或配置中心,破坏容器编译缓存;应把“是否启用”放到功能开关,而非改优先级。 - 与 Laravel 管道(Pipeline)差异?
Pipeline 通过可调用数组顺序执行,同样支持 priority,但需手动 sort;Symfony 把排序下沉到编译期,更适合超大服务地图。 - 在 swoole/roadrunner 常驻进程下,如何利用 TaggedIterator 做热重载?
重启 worker 前重新编译容器,保证优先级变更即时生效;结合 opcache.preload 可做到毫秒级重启。