Mastering Dependency Injection with Laravel's Service Container

Mastering Dependency Injection with Laravel's Service Container

Laravel's service container is a powerful tool for managing class dependencies and performing dependency injection. It's at the heart of many Laravel features and can significantly improve your application's flexibility and testability. Let's explore how to leverage the service container in your Laravel applications.

Basic Binding

At its simplest, the service container is used to bind interfaces to implementations:

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);

This binding tells Laravel to use StripePaymentGateway whenever a PaymentGateway is requested.

Resolving from the Container

You can resolve instances from the container in several ways:

// Using the app() helper
$payment = app(PaymentGateway::class);

// Using the container directly
$payment = $this->app->make(PaymentGateway::class);

// Type-hinting in constructor or method injection
public function __construct(PaymentGateway $payment)
{
    $this->payment = $payment;
}

Singleton Bindings

If you want the same instance to be returned every time, use singleton bindings:

$this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);

Binding Instances

You can bind an existing instance into the container:

$payment = new StripePaymentGateway('secret-key');
$this->app->instance(PaymentGateway::class, $payment);

Contextual Binding

Sometimes you might want different implementations based on the class requesting the dependency:

$this->app->when(PremiumSubscription::class)
          ->needs(PaymentGateway::class)
          ->give(StripePaymentGateway::class);

$this->app->when(BasicSubscription::class)
          ->needs(PaymentGateway::class)
          ->give(PayPalPaymentGateway::class);

Binding Primitives

You can even bind primitives when a class is resolved:

$this->app->when(StripePaymentGateway::class)
          ->needs('$apiKey')
          ->give(config('services.stripe.secret'));

Tagging

The container allows you to tag related bindings:

$this->app->bind(StripePaymentGateway::class, function () {
    // ...
});
$this->app->bind(PayPalPaymentGateway::class, function () {
    // ...
});

$this->app->tag([StripePaymentGateway::class, PayPalPaymentGateway::class], 'gateways');

// Resolve all tagged instances
$gateways = $this->app->tagged('gateways');

Extending Bindings

You can extend existing bindings:

$this->app->extend(PaymentGateway::class, function ($service, $app) {
    return new PaymentGatewayDecorator($service);
});

Real-World Example

Here's how you might use the service container in a payment processing scenario:

interface PaymentGateway
{
    public function charge($amount);
}

class StripePaymentGateway implements PaymentGateway
{
    public function charge($amount) { /* ... */ }
}

class PayPalPaymentGateway implements PaymentGateway
{
    public function charge($amount) { /* ... */ }
}

// In a service provider
public function register()
{
    $this->app->bind(PaymentGateway::class, function ($app) {
        return $app->make(config('services.payment.driver'));
    });
}

// In a controller
public function processPayment(PaymentGateway $gateway)
{
    $gateway->charge(100);
}

Laravel's service container is a fundamental tool for building flexible, testable applications. By leveraging dependency injection and the service container, you can create more modular code, easily swap implementations, and greatly simplify testing.

If this guide was helpful to you, subscribe to my daily newsletter and give me a follow on X/Twitter. It helps a lot!

Subscribe to Harris Raftopoulos

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe