<?php
namespace App\Payment;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.payment_gateway')]
interface PaymentGatewayInterface
{
public static function getName(): string;
public function charge(int $amountCents, string $currency, string $token): ChargeResult;
}
<?php
namespace App\DependencyInjection\Compiler;
use App\Payment\PaymentGatewayInterface;
use App\Payment\PaymentGatewayRegistry;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
final class PaymentGatewayPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition(PaymentGatewayRegistry::class)) {
return;
}
$gateways = [];
foreach ($container->findTaggedServiceIds('app.payment_gateway') as $id => $tags) {
$definition = $container->getDefinition($id);
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (!is_a($class, PaymentGatewayInterface::class, true)) {
throw new \LogicException(sprintf('Service "%s" must implement %s.', $id, PaymentGatewayInterface::class));
}
$name = $class::getName();
$gateways[$name] = new ServiceClosureArgument(new Reference($id));
}
$locator = ServiceLocatorTagPass::register($container, $gateways);
$container->getDefinition(PaymentGatewayRegistry::class)
->setArgument('$locator', $locator);
}
}
<?php
namespace App\Payment;
use Psr\Container\ContainerInterface;
final class PaymentGatewayRegistry
{
public function __construct(private readonly ContainerInterface $locator)
{
}
public function get(string $name): PaymentGatewayInterface
{
if (!$this->locator->has($name)) {
throw new \InvalidArgumentException(sprintf(
'Unknown payment gateway "%s". Available: %s.',
$name,
implode(', ', $this->names())
));
}
return $this->locator->get($name);
}
public function names(): array
{
return array_keys($this->locator->getProvidedServices());
}
}
<?php
namespace App;
use App\DependencyInjection\Compiler\PaymentGatewayPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new PaymentGatewayPass());
}
}
This snippet shows the idiomatic Symfony way to discover a set of interchangeable strategy services at container-build time and expose them lazily through a service locator. The domain is payment gateways: several PaymentGatewayInterface implementations exist (Stripe, PayPal, etc.), each keyed by a short name, and the application needs to resolve one by name at runtime without knowing all of them up front.
In PaymentGatewayInterface, the contract is minimal: a static getName() used as the locator key plus a charge() method returning a ChargeResult. Declaring getName() on the interface lets the compiler pass derive the tag key automatically, so services do not have to repeat their name in configuration. The #[AutoconfigureTag] attribute means every class implementing the interface is tagged app.payment_gateway without any manual services.yaml wiring.
The heart of the mechanism is PaymentGatewayPass, a CompilerPassInterface registered in the kernel. During compilation it calls findTaggedServiceIds() to enumerate all tagged gateways, reads each service's class, and asks the class for its getName() to build a map of name to ServiceClosureArgument (a reference wrapped so the service is instantiated only when actually pulled). That map is handed to ServiceLocatorTagPass::register(), which returns a locator reference bound as the first argument of PaymentGatewayRegistry. Building this map at compile time is the whole point: the container is optimized and cached, so runtime resolution is a cheap array lookup with zero reflection.
The trade-off is that the locator is lazy but fixed — gateways are enumerated once during the build, so adding one requires a container rebuild, which is exactly what production expects. PaymentGatewayRegistry receives the ContainerInterface locator via the #[AutoconfigureTag]-driven wiring and exposes get(), which throws a clear InvalidArgumentException when a name is unknown rather than failing deep inside the container.
This pattern is the canonical answer whenever a plugin-style collection of implementations must be selected by a runtime value; it avoids injecting every implementation eagerly and keeps registration declarative. The main pitfall is forgetting to register the pass in the kernel, in which case the locator argument stays empty and every lookup fails.
Related snips
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
<?php
namespace App\Event;
use Symfony\Contracts\EventDispatcher\Event;
Symfony: Send a Welcome Email Asynchronously with Messenger and an Event Subscriber
import { ApplicationConfig } from '@angular/core';
import {
provideHttpClient,
withInterceptors,
} from '@angular/common/http';
import { retryInterceptor } from './retry.interceptor';
Angular HttpInterceptor With Exponential Backoff and Jittered Retries
class CreateStripeEvents < ActiveRecord::Migration[7.1]
def change
create_table :stripe_events do |t|
t.string :stripe_event_id, null: false
t.string :event_type, null: false
t.string :status, null: false, default: "received"
Idempotent Stripe Webhook Processing in Rails with a Durable Event Log
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
class CreateUserRequest(BaseModel):
model_config = {"extra": "forbid"}
Validating JSON Payloads with Pydantic v2 and Returning Field-Level Errors in FastAPI
Share this code
Here's the card — post it anywhere.