Laravel Framework

Service Container & Dependency Injection

20 min Lesson 23 of 45

Service Container & Dependency Injection

Laravel's Service Container is one of its most powerful features, managing class dependencies and performing dependency injection throughout the framework. Understanding the container is key to writing maintainable, testable, and loosely coupled code.

What is the Service Container?

The Service Container (also called IoC Container - Inversion of Control) is a powerful tool for managing class dependencies. Instead of manually creating objects and their dependencies, the container handles this automatically.

Without Container (Manual Dependency Management):
// UserController.php - Manual approach
class UserController extends Controller
{
    public function index()
    {
        // Manually creating dependencies
        $config = new Configuration();
        $logger = new Logger($config);
        $database = new Database($config);
        $repository = new UserRepository($database, $logger);

        $users = $repository->all();

        return view('users.index', compact('users'));
    }
}

// Problems:
// 1. Tightly coupled to concrete implementations
// 2. Difficult to test (can't easily mock dependencies)
// 3. Repetitive code
// 4. Hard to maintain when dependencies change
With Container (Automatic Dependency Injection):
// UserController.php - Using dependency injection
class UserController extends Controller
{
    protected $users;

    // Container automatically resolves UserRepository and its dependencies
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    public function index()
    {
        $users = $this->users->all();

        return view('users.index', compact('users'));
    }
}

// Benefits:
// 1. Loosely coupled - depends on interface, not implementation
// 2. Easy to test - can inject mock objects
// 3. Clean code - no manual instantiation
// 4. Flexible - swap implementations easily
Key Concept: Dependency Injection means passing dependencies to a class rather than having the class create them. The Service Container automates this process by automatically resolving and injecting dependencies.

Binding to the Container

Binding tells the container how to resolve a particular class or interface. This is typically done in service providers:

Basic Binding:
// app/Providers/AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\PaymentGateway;
use App\Services\StripePaymentGateway;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Simple binding - create new instance each time
        $this->app->bind(PaymentGateway::class, function ($app) {
            return new StripePaymentGateway(
                config('services.stripe.key')
            );
        });

        // Singleton - create once, reuse everywhere
        $this->app->singleton(DatabaseConnection::class, function ($app) {
            return new DatabaseConnection(
                config('database.default')
            );
        });

        // Scoped - singleton within HTTP request
        $this->app->scoped(ShoppingCart::class, function ($app) {
            return new ShoppingCart(session()->getId());
        });

        // Instance binding - bind existing instance
        $apiClient = new ApiClient('https://api.example.com');
        $this->app->instance(ApiClient::class, $apiClient);
    }
}
Choosing Binding Types:
  • bind(): New instance every time (default for most services)
  • singleton(): One instance for entire application (database, cache, logger)
  • scoped(): One instance per request (shopping cart, user session data)
  • instance(): Bind a pre-existing object

Automatic Resolution and Type-Hinting

Laravel's container can automatically resolve classes without explicit binding if they have no constructor dependencies or only have dependencies that are also auto-resolvable:

Automatic Resolution:
// These classes are automatically resolvable

// No dependencies
class SimpleService
{
    public function execute()
    {
        return 'Executed';
    }
}

// Resolvable dependencies (concrete classes)
class UserService
{
    public function __construct(
        protected Logger $logger,
        protected UserRepository $repository
    ) {}
}

// Using in controller - no binding needed
class UserController extends Controller
{
    public function __construct(
        protected UserService $userService,
        protected SimpleService $simpleService
    ) {}

    public function index()
    {
        $this->simpleService->execute();
        return $this->userService->getUsers();
    }
}

// Container automatically:
// 1. Resolves SimpleService (no dependencies)
// 2. Resolves Logger and UserRepository for UserService
// 3. Creates UserService with its dependencies
// 4. Injects everything into UserController

Interface to Implementation Binding

Binding interfaces to implementations is a key pattern for maintainable applications:

Interface Binding:
// Define interface
namespace App\Contracts;

interface PaymentGatewayInterface
{
    public function charge(int $amount): bool;
    public function refund(string $transactionId): bool;
}

// Implementation
namespace App\Services;

use App\Contracts\PaymentGatewayInterface;

class StripePaymentGateway implements PaymentGatewayInterface
{
    public function charge(int $amount): bool
    {
        // Stripe-specific implementation
        return true;
    }

    public function refund(string $transactionId): bool
    {
        // Stripe-specific implementation
        return true;
    }
}

// Bind interface to implementation in service provider
public function register(): void
{
    $this->app->bind(
        PaymentGatewayInterface::class,
        StripePaymentGateway::class
    );
}

// Use in controller - depends on interface, not implementation
class PaymentController extends Controller
{
    public function __construct(
        protected PaymentGatewayInterface $gateway
    ) {}

    public function charge(Request $request)
    {
        $success = $this->gateway->charge($request->amount);

        return response()->json(['success' => $success]);
    }
}

// Benefits:
// 1. Can swap StripePaymentGateway for PayPalPaymentGateway
// 2. Easy to mock in tests
// 3. Code depends on abstraction, not concrete class
Important: Always type-hint interfaces or abstract classes in your constructors and methods, not concrete implementations. This makes your code flexible and testable.

Manual Resolution from Container

Sometimes you need to manually resolve instances from the container:

Resolving from Container:
// Using app() helper
$userService = app(UserService::class);

// Using App facade
use Illuminate\Support\Facades\App;
$userService = App::make(UserService::class);

// Using resolve() helper
$userService = resolve(UserService::class);

// Resolving with parameters
$report = app(ReportGenerator::class, [
    'startDate' => now()->subMonth(),
    'endDate' => now()
]);

// Calling methods with dependency injection
$result = app()->call(function (UserService $service) {
    return $service->getActiveUsers();
});

// Calling class methods
$result = app()->call([UserService::class, 'getActiveUsers']);

// Method injection in controllers
class ReportController extends Controller
{
    // Dependencies injected into method, not constructor
    public function generate(
        Request $request,
        ReportGenerator $generator,
        PdfExporter $exporter
    ) {
        $report = $generator->create($request->all());
        return $exporter->export($report);
    }
}

Contextual Binding

Sometimes you need different implementations of an interface depending on which class needs it:

Contextual Binding:
// Two classes need different cache implementations
namespace App\Services;

class UserService
{
    public function __construct(CacheInterface $cache) {}
}

class ReportService
{
    public function __construct(CacheInterface $cache) {}
}

// In service provider - bind different implementations contextually
public function register(): void
{
    // UserService gets RedisCache
    $this->app->when(UserService::class)
              ->needs(CacheInterface::class)
              ->give(function () {
                  return new RedisCache();
              });

    // ReportService gets FileCache
    $this->app->when(ReportService::class)
              ->needs(CacheInterface::class)
              ->give(function () {
                  return new FileCache();
              });

    // Multiple classes can share same implementation
    $this->app->when([
        OrderService::class,
        InvoiceService::class,
        ShippingService::class
    ])
    ->needs(LoggerInterface::class)
    ->give(DatabaseLogger::class);
}

// Contextual binding with primitives
$this->app->when(DatabaseConnection::class)
          ->needs('$host')
          ->give(config('database.host'));

$this->app->when(ApiClient::class)
          ->needs('$timeout')
          ->giveConfig('services.api.timeout');  // Shortcut for config values
Use Case: Contextual binding is perfect when different parts of your app need different configurations or implementations of the same interface (e.g., different payment gateways for different regions, different cache drivers for different services).

Facades vs Dependency Injection

Laravel provides both facades and dependency injection. Understanding when to use each:

Facades vs DI:
// Using Facade - static-like interface
use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    public function index()
    {
        $users = Cache::remember('users', 3600, function () {
            return User::all();
        });

        return view('users.index', compact('users'));
    }
}

// Using Dependency Injection - injecting contract
use Illuminate\Contracts\Cache\Repository as CacheRepository;

class UserController extends Controller
{
    public function __construct(
        protected CacheRepository $cache
    ) {}

    public function index()
    {
        $users = $this->cache->remember('users', 3600, function () {
            return User::all();
        });

        return view('users.index', compact('users'));
    }
}

// When to use each:

// Facades - Good for:
// - Quick prototyping
// - Simple scripts and commands
// - One-off usage in a method
// - When testability isn't a primary concern

// Dependency Injection - Good for:
// - Classes that will be unit tested
// - When you need to swap implementations
// - Complex services with many dependencies
// - Following SOLID principles strictly
Testing Note: Both facades and dependency injection are testable in Laravel. Facades have built-in testing helpers, but DI makes it easier to inject mock objects in unit tests.

Container Events and Resolving Callbacks

The container fires events when resolving classes, allowing you to configure objects after creation:

Resolving and Extending:
// In service provider

// Run callback after resolving a class
$this->app->resolving(PaymentService::class, function ($service, $app) {
    // Configure the service after it's created
    $service->setApiKey(config('services.payment.key'));
    $service->setEnvironment(app()->environment());
});

// Run callback for all resolutions
$this->app->resolving(function ($object, $app) {
    // Called for every object resolved from container
    if (method_exists($object, 'setLogger')) {
        $object->setLogger($app->make(LoggerInterface::class));
    }
});

// Extend an existing binding
$this->app->extend(ApiClient::class, function ($service, $app) {
    // Modify the service after it's created
    $service->addMiddleware(new RateLimitMiddleware());
    return $service;
});

// AfterResolving - similar to resolving but runs after all resolving callbacks
$this->app->afterResolving(UserService::class, function ($service, $app) {
    $service->boot();
});
Practice Exercise 1: Create a NotificationService that depends on a NotificationChannelInterface. Create two implementations: EmailChannel and SmsChannel. Bind the interface to EmailChannel by default, but use contextual binding so that UrgentAlertService uses SmsChannel while RegularAlertService uses EmailChannel.
Practice Exercise 2: Build a ReportGenerator class that depends on PdfExporter, DataRepository, and Logger interfaces. Create concrete implementations and bind them in a service provider. Then create a controller that uses ReportGenerator through dependency injection to generate and download a PDF report.
Practice Exercise 3: Create a CacheManager singleton that tracks cache hits and misses. Use the container's resolving callback to automatically inject this CacheManager into any class that has a setCacheManager() method. Test it by creating two different services that both receive the same CacheManager instance.

Summary

The Service Container is the heart of Laravel's architecture:

  • Dependency Injection: Automatically resolves and injects class dependencies
  • Binding: Tell the container how to create objects (bind, singleton, scoped)
  • Interface Binding: Program to interfaces for flexible, testable code
  • Contextual Binding: Different implementations for different contexts
  • Automatic Resolution: No binding needed for simple classes
  • Facades vs DI: Both are valid; choose based on use case

In the next lesson, we'll explore Service Providers, which are the central place for container bindings and application bootstrapping.