How does Laravel handle dependency injection?
Mastering Dependency Injection in Laravel: From Fundamentals to ArchitectureThe distinction between a script that merely functions and a professional application that scales lies in its arc...
Written by Mukesh · Reviewed Aug 20, 2026
new Service()) creates a rigid hierarchy that is difficult to maintain.- Direct Instantiation (new Service()):
- Brittle Testing: You cannot swap a production service for a "mock" during testing, often leading to tests that hit real APIs or databases.
- Side Effects: A change in a low-level driver (like switching an SMS provider) creates a ripple effect, forcing modifications in high-level business logic.
- Hidden Dependencies: Requirements are buried inside method bodies rather than being explicitly declared.
- Injected Flexibility:
- Architectural Swappability: Easily replace implementations without touching the consuming class.
- Isolation: Test business logic in total isolation by injecting "fakes."
- Declarative Design: A class’s needs are visible at the signature level, serving as living documentation.
- Inspection: The container inspects the class constructor to identify type-hinted dependencies.
- Resolution: It checks its registry to see if a specific binding exists for those types.
- Recursive Injection: This is the container's true power. If a service needs a client, and that client needs a configuration object, Laravel resolves the entire dependency tree automatically.
- Caching: If defined as a singleton, the container stores the instance for subsequent requests within the same lifecycle.
// The Container handles this entire tree recursively:
class ConfigRepository { /* ... */ }
class ApiClient {
public function __construct(protected ConfigRepository $config) {}
}
class AnalyticsService {
public function __construct(protected ApiClient $client) {}
}
// Laravel resolves the service, the client, and the config automatically.
Route::get('/stats', function (AnalyticsService $service) {
return $service->report();
});
class OrderController extends Controller
{
public function __construct(
protected PaymentGateway $gateway // Injected and stored
) {}
public function store() {
return $this->gateway->charge(100);
}
}
public function export(Order $order, PdfGenerator $pdf) // Surgical injection
{
return $pdf->generate($order);
}
Criterion | Constructor Injection | Method Injection |
|---|---|---|
Scope of Use | Class-wide availability | Local to a single method |
Class State | Persistent (stored as property) | Transient (passed as argument) |
Stateful vs. Stateless | Ideal for Stateful Services | Ideal for Stateless Utilities |
Primary Use Case | Repositories, Loggers, Core Services | Exporting, One-time API calls |
interface PaymentGateway {
public function charge(float $amount): bool;
}
class StripeGateway implements PaymentGateway {
public function charge(float $amount): bool { /* Stripe Logic */ }
}
$this->app->bind(PaymentGateway::class, StripeGateway::class);
StripeGateway for a FakePaymentGateway during tests. This ensures your suite is lightning-fast and side-effect-free, requiring zero changes to your controller logic.- Bind: Creates a fresh instance every time. Use this for stateless services where no data needs to persist between injections.
- Singleton: Resolves once and reuses the instance. Ideal for stateful services or expensive objects like database connections.
- Scoped: Reuses an instance within a specific lifecycle (e.g., an Octane request). It is flushed before the next lifecycle starts.
public function register(): void
{
// Fresh instance every time
$this->app->bind(ProcessorInterface::class, BatchProcessor::class);
// One shared instance
$this->app->singleton(Connection::class, fn() => new Connection(config('db')));
}
- #[Bind] and #[Singleton]: Applied directly to classes or interfaces to handle registration without a Service Provider.
- Directory Shift: In Laravel 13, the
bootstrap/providers.phpfile has replaced the oldconfig/app.phpproviders array for cleaner registration management.
- Asymmetric Visibility: Using
public private(set)allows a property to be publicly readable but only modifiable within its own class. This prevents listeners or external services from accidentally mutating event data or DTOs. - Property Hooks: Replaces manual getters/setters with native
getandsetlogic, keeping your services lean.
- Interface:
PaymentGatewaydefines the contract. - Logic:
StripePaymentandPayPalPaymentimplement the logic. - Dynamic Binding: The Service Provider acts as the bridge to the environment.
// PaymentServiceProvider.php
public function register(): void
{
$this->app->bind(PaymentGateway::class, function ($app) {
$driver = config('services.payment.default'); // Bridge to .env
return match($driver) {
'stripe' => new StripePayment(config('services.stripe.key')),
'paypal' => new PayPalPayment(config('services.paypal.id')),
};
});
}
match expression to config('services.payment.default'), the business can transform its entire commerce logic by changing one line in the .env file (PAYMENT_GATEWAY=paypal). No UI or Controller code is ever touched.- Over-Injection: The "7-8 Dependencies Rule." If a class requires more than 7 dependencies, it is likely a "God Object" violating the Single Responsibility Principle. Decompose it into smaller services.
- Circular Dependencies: Class A needs Class B, which needs Class A. This creates a resolution loop that the container cannot break.
- Service Locator Anti-Pattern: Avoid using
app()->make()orresolve()inside your methods. This hides dependencies and makes the class impossible to test without a fully booted framework. Always prefer explicit injection.
- How does the container work? It's a sophisticated factory using the Reflection API to see what a class needs and resolving the entire dependency tree recursively.
- Do I need to register every class? No. "Zero Configuration" handles concrete classes automatically. Registration is for interfaces, singletons, or custom configuration.
- What is Contextual Binding? It allows you to provide different implementations of the same interface to different consumers (e.g., giving the
S3driver toPhotoServicebut theLocaldriver toLogService).
About the author
Mukesh is the developer behind InfoMukesh, writing practical notes from hands-on work with PHP, Laravel, e-commerce platforms, AI, and web applications.