Service Container & Dependency Injection
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.
// 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
// 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
Binding to the Container
Binding tells the container how to resolve a particular class or interface. This is typically done in service providers:
// 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);
}
}
- 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:
// 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:
// 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
Manual Resolution from Container
Sometimes you need to manually resolve instances from the 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:
// 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
Facades vs Dependency Injection
Laravel provides both facades and dependency injection. Understanding when to use each:
// 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
Container Events and Resolving Callbacks
The container fires events when resolving classes, allowing you to configure objects after creation:
// 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();
});
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.