Dynamic Service Container Tagging for Runtime Logic Switching

What is it? Did you know that Laravel’s Service Container supports tagging for grouping multiple implementations of an interface or class? While tagging itself is documented, here’s the twist:

You can use dynamic tagging and runtime resolution to select implementations on the fly, making your services incredibly flexible without hardcoding anything.

The Hack

  1. Define a Base Interface

Let’s say you have an interface for payment gateways:

1namespace App\Contracts;
2 
3interface PaymentGateway
4{
5 public function process($amount);
6}
  1. Create Multiple Implementations

Define a couple of implementations for this interface:

1namespace App\Services\Payments;
2 
3use App\Contracts\PaymentGateway;
4 
5class StripeGateway implements PaymentGateway
6{
7 public function process($amount)
8 {
9 return "Processing {$amount} via Stripe!";
10 }
11}
12 
13class PayPalGateway implements PaymentGateway
14{
15 public function process($amount)
16 {
17 return "Processing {$amount} via PayPal!";
18 }
19}
  1. Tag Services in a Service Provider

In your AppServiceProvider, tag these implementations dynamically:

1use App\Contracts\PaymentGateway;
2use App\Services\Payments\StripeGateway;
3use App\Services\Payments\PayPalGateway;
4 
5public function register()
6{
7 $this->app->bind(StripeGateway::class, fn () => new StripeGateway());
8 $this->app->bind(PayPalGateway::class, fn () => new PayPalGateway());
9 
10 // Dynamically tag both services
11 $this->app->tag([StripeGateway::class, PayPalGateway::class], 'payment_gateways');
12}
  1. Resolve Dynamically Based on Logic

Now you can resolve and switch between implementations at runtime based on dynamic conditions:

1use Illuminate\Contracts\Container\Container;
2 
3function getPaymentGateway(Container $container, $preferredGateway): PaymentGateway
4{
5 $gateways = $container->tagged('payment_gateways');
6 
7 foreach ($gateways as $gateway) {
8 if (get_class($gateway) === $preferredGateway) {
9 return $gateway;
10 }
11 }
12 
13 throw new \Exception("No gateway found for {$preferredGateway}");
14}
15 
16// Example usage
17$gatewayClass = \App\Services\Payments\StripeGateway::class; // Could come from config or runtime logic
18 
19$gateway = getPaymentGateway(app(), $gatewayClass);
20 
21echo $gateway->process(100); // Outputs: Processing 100 via Stripe!
  1. Bonus Hack: Auto-Tag Based on Folder Scanning

Let’s take it a step further and auto-tag classes in a folder:

1use Illuminate\Support\Facades\File;
2 
3public function register()
4{
5 $paymentServices = collect(File::allFiles(app_path('Services/Payments')))
6 ->map(fn ($file) => 'App\\Services\\Payments\\' . $file->getFilenameWithoutExtension())
7 ->filter(fn ($class) => class_exists($class))
8 ->each(fn ($class) => $this->app->bind($class, fn () => new $class()));
9 
10 $this->app->tag($paymentServices->all(), 'payment_gateways');
11}

Now, any new class in App\Services\Payments is automatically tagged and ready to use.

Diagram