The container is built once and then frozen, and a compiler pass is the hook that runs during that build — which is the only moment you can see every service definition at once.
final class RegisterHandlersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$registry = $container->findDefinition(HandlerRegistry::class);
foreach ($container->findTaggedServiceIds('app.handler') as $id => $tags) {
$registry->addMethodCall('register', [new Reference($id)]);
}
}
}
The legitimate use is collecting tagged services into a registry, and autoconfigure plus a tagged iterator argument now covers most of that case without writing one. Reach for a pass when the collection needs ordering by a tag attribute, or when a definition must be modified rather than merely gathered. Anything done here happens at build time, so it costs nothing per request — and it fails at cache warm rather than at the request that needed it, which is the right place to fail.