The service container
Automatic dependency injection, binding and resolving, singletons and contextual bindings β the machine the whole framework is built on.
The container is a factory that knows how to build your objects, including everything those objects need. Understanding it is what makes the rest of Laravel stop feeling like magic.
Zero-configuration resolution
If a class's dependencies are all concrete classes, the container can build it with no configuration at all.
<?php
class Mailer
{
public function send(string $to): string
{
return "mail to $to";
}
}
class OrderService
{
public function __construct(private Mailer $mailer) {}
public function confirm(string $email): string
{
return 'confirmed: ' . $this->mailer->send($email);
}
}
$service = app(OrderService::class); // Mailer built automatically
echo $service->confirm('ada@example.com'), PHP_EOL;Nothing was registered. The container read the constructor's type hints with reflection and worked backwards.
<?php
class NeedsConfig
{
public function __construct(private string $apiKey) {}
}
try {
app(NeedsConfig::class);
} catch (\Throwable $e) {
echo get_class($e), ': unresolvable scalar', PHP_EOL;
}Binding an interface to an implementation
This is the container's real job: your code depends on an interface, and one line decides what that means.
<?php
interface PaymentGateway
{
public function charge(int $cents): string;
}
class StripeGateway implements PaymentGateway
{
public function charge(int $cents): string
{
return "stripe charged $cents";
}
}
class FakeGateway implements PaymentGateway
{
public function charge(int $cents): string
{
return "pretended to charge $cents";
}
}
class Checkout
{
public function __construct(private PaymentGateway $gateway) {}
public function pay(int $cents): string
{
return $this->gateway->charge($cents);
}
}
app()->bind(PaymentGateway::class, StripeGateway::class);
echo app(Checkout::class)->pay(500), PHP_EOL;
app()->bind(PaymentGateway::class, FakeGateway::class); // swap for tests
echo app(Checkout::class)->pay(500), PHP_EOL;Checkout never changed. That is the whole point.
bind versus singleton
bind runs the factory every time. singleton runs it once and reuses the instance.
<?php
class Counter
{
public int $n = 0;
}
app()->bind('fresh', fn () => new Counter());
app()->singleton('shared', fn () => new Counter());
app('fresh')->n++;
app('fresh')->n++;
echo 'bind: ', app('fresh')->n, PHP_EOL; // 0 β a new object each time
app('shared')->n++;
app('shared')->n++;
echo 'singleton: ', app('shared')->n, PHP_EOL; // 2 β the same objectinstance() registers an object you already have; scoped() is a singleton that resets per request β the one to reach for under Octane.
<?php
class Config
{
public function __construct(public string $env) {}
}
app()->instance('my.config', new Config('staging'));
echo app('my.config')->env, PHP_EOL;
var_dump(app('my.config') === app('my.config'));β οΈ A singleton holding request-specific state is the classic Octane bug: it is built during request 1 and still holds request 1's user during request 500. Under a traditional PHP-FPM setup the process dies and hides the problem; under a long-lived worker it does not. scoped() exists for exactly this.
The factory closure
When construction needs more than type hints, bind a closure. It receives the container.
<?php
class HttpClient
{
public function __construct(public string $baseUrl, public int $timeout) {}
}
app()->singleton(HttpClient::class, function ($app) {
return new HttpClient(
baseUrl: $app['config']->get('app.url', 'http://localhost'),
timeout: 5,
);
});
$client = app(HttpClient::class);
echo $client->baseUrl, ' (timeout ', $client->timeout, "s)", PHP_EOL;Contextual binding
Two classes, the same interface, different implementations β decided by who is asking.
<?php
interface Storage
{
public function put(string $key): string;
}
class LocalStorage implements Storage
{
public function put(string $key): string { return "local:$key"; }
}
class CloudStorage implements Storage
{
public function put(string $key): string { return "cloud:$key"; }
}
class AvatarUploader
{
public function __construct(public Storage $storage) {}
}
class LogArchiver
{
public function __construct(public Storage $storage) {}
}
app()->when(AvatarUploader::class)->needs(Storage::class)->give(CloudStorage::class);
app()->when(LogArchiver::class)->needs(Storage::class)->give(LocalStorage::class);
echo app(AvatarUploader::class)->storage->put('a.png'), PHP_EOL;
echo app(LogArchiver::class)->storage->put('app.log'), PHP_EOL;Method injection
The container can also fill a method's parameters, which is what makes controller actions work.
<?php
class Mailer
{
public function send(string $to): string { return "sent to $to"; }
}
class Report
{
public function generate(Mailer $mailer, string $to): string
{
return $mailer->send($to);
}
}
echo app()->call([new Report(), 'generate'], ['to' => 'ada@example.com']), PHP_EOL;A controller action is resolved the same way: type-hinted classes come from the container, and route parameters are matched by name.
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
class Clock
{
public function now(): string { return '2026-01-15'; }
}
Route::get('/report/{name}', function (Request $request, Clock $clock, string $name) {
return "$name at {$clock->now()} from {$request->path()}";
});
echo visit('/report/sales')->getContent(), PHP_EOL;π‘ Tip: Notice the order does not matter. The container matches by type for classes and by name for route parameters, so you can list them however reads best.
Tagging
Register a group, resolve it as a list β useful for plugin-style collections.
<?php
interface Rule
{
public function check(string $v): bool;
}
class NotEmpty implements Rule
{
public function check(string $v): bool { return $v !== ''; }
}
class MaxTen implements Rule
{
public function check(string $v): bool { return strlen($v) <= 10; }
}
app()->bind(NotEmpty::class);
app()->bind(MaxTen::class);
app()->tag([NotEmpty::class, MaxTen::class], 'rules');
foreach (app()->tagged('rules') as $rule) {
echo get_class($rule), ': ', var_export($rule->check('hello'), true), PHP_EOL;
}When not to use it
The container is for services β things with behaviour and dependencies. It is not for data.
<?php
// Wrong: a value object built from request data does not belong in the container.
final class Money
{
public function __construct(public readonly int $cents) {}
}
$m = new Money(500); // just use new
echo $m->cents, PHP_EOL;Reference: Service Container in the Laravel documentation.