Service Container & Dependency Injection in Laravel
Service Container & Dependency Injection: Laravel's Heart
Reading Time: 30 minutes
Introduction
The Laravel Service Container is the framework's central nervous system — it manages class dependencies, performs dependency injection, and controls object instantiation. Understanding the container is essential for writing testable, decoupled, and maintainable Laravel applications.
Many developers use the container daily without realizing it: route model binding, controller method injection, queued jobs, and event listeners are all resolved through the container.
What Is Dependency Injection?
Dependency Injection (DI) is a design pattern where a class receives its dependencies from the outside rather than creating them internally.
// ❌ Without DI — hardcoded dependency
class UserController
{
private UserService $service;
public function __construct()
{
$this->service = new UserService(new MySQLUserRepository());
}
}
// ✅ With DI — dependencies injected
class UserController
{
public function __construct(
private UserService $service
) {}
}
Benefits of DI:
| Benefit | Description |
|---|---|
| Testability | Swap real implementations with mocks |
| Decoupling | Classes depend on interfaces, not concretions |
| Flexibility | Change behavior without modifying consumers |
| Reusability | Same class works in different contexts |
The Container in Action
graph TD
A[Your Code] --> B[resolve / app->make]
A --> C[Automatic Injection]
B --> D[Service Container]
C --> D
D --> E{Binding Exists?}
E -->|Yes| F[Resolve from Binding]
E -->|No| G[Reflection Resolve]
G --> H[Build Dependencies]
H --> I[Return Instance]
F --> I
I --> J[Cached if singleton]
Zero-Configuration Resolution
The container can resolve classes without any explicit binding — as long as the class has no unresolvable dependencies:
class SimpleService
{
public function doSomething(): string
{
return 'Done!';
}
}
// No binding needed
$service = app(SimpleService::class);
Automatic Injection in Controllers
class UserController extends Controller
{
public function __construct(
private UserRepository $repository
) {}
public function show(string $id): JsonResponse
{
$user = $this->repository->find($id);
return response()->json($user);
}
}
Laravel automatically resolves UserRepository from the container when the controller is instantiated.
Binding Types
Simple Binding (bind)
Every time you resolve, you get a new instance:
$this->app->bind(HelpSpot\API::class, function ($app) {
return new HelpSpot\API($app->config->get('helpspot.api_key'));
});
Singleton Binding (singleton)
The same instance is returned on subsequent resolutions:
$this->app->singleton(HelpSpot\API::class, function ($app) {
return new HelpSpot\API($app->config->get('helpspot.api_key'));
});
When to use singleton:
- Stateless services — HTTP clients, API wrappers, mailers
- Configuration holders — settings, feature flags
- Expensive to instantiate — database connections, SDK clients
- Event dispatchers — should have one instance
When NOT to use singleton:
- Stateful services — if the service holds per-request state, each request needs a fresh instance
- Eloquent models — always new instances
Instance Binding (instance)
Bind an already-existing object:
$api = new HelpSpot\API('token');
$this->app->instance(HelpSpot\API::class, $api);
Scoped Binding (scoped)
Same instance within the same container scope (useful for Octane/long-running processes):
$this->app->scoped(HelpSpot\API::class, function ($app) {
return new HelpSpot\API($app->config->get('helpspot.api_key'));
});
In Octane, scoped ensures each request gets its own instance, unlike singleton which persists across requests.
Binding Primitives
When a class needs a primitive value (string, int, array):
$this->app->when(EmailService::class)
->needs('$apiKey')
->give(config('services.mailgun.api_key'));
$this->app->when(ReportGenerator::class)
->needs('$columns')
->give(['name', 'email', 'created_at']);
Binding Interfaces to Implementations
This is the most powerful pattern for decoupling:
// Interface
interface PaymentGateway
{
public function charge(int $amount, array $details): Transaction;
}
// Implementation
class StripeGateway implements PaymentGateway
{
public function charge(int $amount, array $details): Transaction
{
// Stripe API call
}
}
// ServiceProvider
$this->app->bind(PaymentGateway::class, StripeGateway::class);
// Controller — depends on interface, not concrete
class CheckoutController extends Controller
{
public function __construct(
private PaymentGateway $gateway
) {}
public function store(Request $request): JsonResponse
{
$transaction = $this->gateway->charge($request->amount, $request->all());
return response()->json($transaction);
}
}
Switching providers becomes a one-line change:
// Switch to PayPal
$this->app->bind(PaymentGateway::class, PayPalGateway::class);
Contextual Binding
When different classes need different implementations of the same interface:
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
Tagging
Group related bindings for batch resolution:
$this->app->tag([
SpeedReport::class,
MemoryReport::class,
PerformanceReport::class,
], 'reports');
// Resolve all tagged bindings
$this->app->tagged('reports');
// Returns: [SpeedReport, MemoryReport, PerformanceReport]
Useful for report generators, event handlers, or any collection of related services.
Extending Resolved Instances
Modify or decorate a service after it has been resolved:
$this->app->extend(PaymentGateway::class, function ($gateway, $app) {
return new LoggingPaymentGateway($gateway, $app->make(Logger::class));
});
The original service (StripeGateway) is passed in — you wrap it with a decorator.
Service Providers
Service providers are the central place to configure the container.
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind interfaces to implementations
$this->app->bind(PaymentGateway::class, StripeGateway::class);
// Register services
$this->app->singleton(HelpSpot\API::class, function ($app) {
return new HelpSpot\API($app->config->get('helpspot.api_key'));
});
}
public function boot(): void
{
// After all providers are registered
// Great place for event listeners, route macros, etc.
}
}
Deferred Providers
Services that are only resolved when needed:
class StripeServiceProvider extends ServiceProvider
{
// Only loaded when Stripe\Client or PaymentGateway is resolved
public function provides(): array
{
return [PaymentGateway::class, Stripe\Client::class];
}
}
Container Events
Resolving Callbacks
Fired every time a type is resolved:
$this->app->resolving(PaymentGateway::class, function ($gateway, $app) {
// Configure the gateway after it's constructed
$gateway->setLogger($app->make(Logger::class));
});
Rebinding Callbacks
Fired when a binding is overridden:
$this->app->rebinding(PaymentGateway::class, function ($app, $gateway) {
// Notify other services that the gateway changed
});
Facades
Facades provide a static-like interface to services resolved from the container.
// Facade
Cache::get('key');
// Is equivalent to:
$app->make('cache')->get('key');
How Facades Work
class Cache extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'cache';
}
}
Cache::get() is resolved at runtime — the facade gets the cache binding from the container and calls get() on it.
Real-Time Facades
Turn any class into a facade:
use Facades\App\Services\PaymentGateway;
PaymentGateway::charge(100, []);
Performance Impact
| Operation | Cost | Frequency |
|---|---|---|
| Reflection resolution (no binding) | Medium | First call only |
| Closure binding | Low | Registration |
| Singleton resolution (cached) | Negligible | Every call |
| Facade resolution | Low | Every call |
In production, the route cache and config cache eliminate most container overhead.
Common Mistakes
Mistake 1: Constructor Overload
// ❌ Too many dependencies — violates Single Responsibility
class OrderController extends Controller
{
public function __construct(
private OrderService $orders,
private PaymentGateway $gateway,
private InventoryService $inventory,
private NotificationService $notifications,
private Logger $logger,
private Mailer $mailer,
private Cache $cache,
private Analytics $analytics,
) {}
}
Fix: Split the controller or use action classes.
Mistake 2: Binding Too Many Things
// ❌ Binding everything "just in case"
$this->app->bind(Logger::class, Logger::class);
$this->app->bind(Cache::class, Cache::class);
Fix: Only bind interfaces, third-party SDKs, or classes that need configuration.
Mistake 3: Using app() in Views or Blade
// ❌ Service locator in template
{{ app(Logger::class)->info('view rendered') }}
Fix: Pass data from the controller via view()->with() or view composers.
Mistake 4: Forgetting to Use Interfaces
// ❌ Registered concrete class — can't easily swap
$this->app->bind(StripeGateway::class, StripeGateway::class);
// ✅ Register interface — swap by changing one line
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Service Container vs Service Locator
| Aspect | Service Container | Service Locator |
|---|---|---|
| How it gets dependencies | Injected via constructor | Pulled from a registry |
| Testability | Excellent (mock via container) | Poor (hidden dependencies) |
| Discoverability | Visible in constructor signature | Hidden inside methods |
| Framework support | Built into Laravel | Pattern used by app() helper |
Recommendation: Prefer constructor injection over resolve() or app()->make().
Interview Questions
- What is the difference between
bindandsingleton? - How does contextual binding work and when would you use it?
- Explain the
register()vsboot()difference in service providers. - What are facades and how do they work under the hood?
- When would you use
tagandtagged? - How does the container resolve classes without explicit binding?
- What is the problem with using
app()inside views?