sergeil/expander-bundle
Symfony bundle that integrates the Expander library into your application, registering services and configuration so you can expand nested values and resolve paths cleanly. It provides straightforward container wiring and defaults for reusable expansion logic across your project.
The sergeil/expander-bundle is a Symfony bundle, not a Laravel package — it does not ship a ServiceProvider, facades, or anything Composer-autoloadable into a Laravel app. Before you write any code, treat it as two things: (1) a drop-in extension-point mechanism if you happen to be on Symfony, and (2) a reference implementation of the tagged-service pattern you may want to port into Laravel.
If you are actually on Symfony:
composer require sergeil/expander-bundle:^1.2
Version note (v1.2.1): this release added Symfony 5 compatibility. That makes v1.2.1 the first version you should consider if your app is on Symfony 5.x — but it also means the bundle's ceiling is Symfony 5. Pin symfony/framework-bundle accordingly and do not assume Symfony 6/7 support; there is no release note claiming it.
Register it in config/bundles.php:
return [
Sergeil\ExpanderBundle\ExpanderBundle::class => ['all' => true],
];
Then look first at three things in this order:
Your first real use case: replacing a hand-rolled match/switch factory. Instead of a PaymentGatewayFactory with a hardcoded match ($type) block, you declare one extension point, tag each gateway service, and inject the collected set.
For a Laravel developer, the closest first step is to read the bundle and then reproduce the idea with Laravel's own container tagging ($this->app->tag([...], 'payments.gateway') + $this->app->bind(GatewayRegistry::class, fn ($app) => new GatewayRegistry($app->tagged('payments.gateway')))). Same concept, no bundle required.
1. Declare an extension point, then tag implementations. Define a single contract (e.g. GatewayInterface) and an extension point class that receives iterable $gateways. Tag every implementation:
# services.yaml
App\Payment\StripeGateway:
tags: ['app.payment_gateway']
Autoconfiguration (Symfony 5.3+) is the cleaner pattern — put the tag on the interface so every implementation is picked up automatically:
#[AutoconfigureTag('app.payment_gateway')]
interface GatewayInterface {}
Because v1.2.1 targets Symfony 5, you are on the framework generation where attribute-based autoconfiguration and _defaults: autoconfigure: true are the intended path — use them, and drop the annotation-style equivalent unless you still support Symfony 4 code paths.
2. Keep ordering in the extension point, not the callers. If the bundle exposes priorities on the tag, use them; otherwise sort deterministically inside your extension point (by a supports() score or an explicit getPriority() on the interface). Never rely on container iteration order.
3. Use a service locator for expensive services. If your tagged services open connections or do heavy constructor work, inject ServiceLocator rather than an eager iterable, otherwise every container boot instantiates all tagged services. This is the single biggest production concern with tag-based registries.
4. Integration entry points. The extension point should be injected into:
php artisan-equivalent bin/console) for listing/selecting registered extensions.5. Testing pattern. Assert against the compiled container rather than the class:
self::assertCount(3, self::getContainer()->get(GatewayRegistry::class)->all());
Then unit-test each tagged implementation in isolation. This keeps the extension point wiring separate from business logic.
6. Porting to Laravel (the realistic path). If your goal is "this pattern in a Laravel app":
// AppServiceProvider::register()
$this->app->singleton(GatewayRegistry::class, function ($app) {
return new GatewayRegistry($app->tagged('payments.gateways'));
});
// AppServiceProvider::boot()
$this->app->tag([StripeGateway::class, PaypalGateway::class], 'payments.gateways');
Or lean on contextual binding if you only ever need one implementation per key:
$this->app->when(CheckoutController::class)
->needs(GatewayInterface::class)
->giveTagged('payments.gateways');
The Laravel pattern is strictly more ergonomic than the Symfony bundle for this job, which is worth noting in your evaluation.
composer.json before composer require, since a permissive-looking tag can still resolve against an incompatible framework-bundle.bin/console cache:clear — stale compiled containers are the #1 source of "my service isn't showing up." In Laravel, same class of bug with php artisan optimize:clear. This is doubly true after a bundle version bump: a container compiled against v1.0 will not pick up v1.2.1's behaviour until you clear._defaults: autoconfigure: true block. Manually defined services with autoconfigure: false silently miss the tag.bin/console debug:container --tag=app.payment_gateway lists tagged services; bin/console lint:container catches wiring errors before runtime. There is no equivalent Laravel command — use php artisan tinker and inspect app()->tagged('...').config/bundles.php produces a "class not found" or silently does nothing, because the compiler pass never runs.GatewayInterface::TAG), not inline strings sprinkled across services.yaml and ServiceProvider files. This is the single highest-leverage tip for both Symfony and the Laravel port.How can I help you explore Laravel packages today?