A compiler pass, and the one time you need one

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.