The service container is Laravel's object factory: you ask for a class, and it builds it, along with everything that class needs, automatically.
๐Think of a restaurant kitchen. You order a burger; you don't bake the bun, grill the patty or slice the tomato yourself. The kitchen knows the recipe, prepares every ingredient in the right order and hands you the finished plate.
The engine behind Laravel's "magic"
It explains the magic
Ever wondered how a Request $request argument just "appears" in your controller? That's the container at work.
Swap parts easily
Depend on an interface, then tell the container which real class to use. Switch from Mailgun to SES by changing one line.
Testable code
Because classes receive their dependencies instead of creating them, tests can hand in fakes and mocks.
The container builds a whole tree of objects
PostController.- Dependency
- Another object a class needs to do its job.
PostPublisherdepends on aSlugGenerator. - Dependency injection (DI)
- Handing a class its dependencies from the outside (usually through the constructor) instead of letting it create them with
new. - Resolving
- Asking the container to build an object for you: "give me a
PostPublisher". - Binding
- A recipe you register that tells the container how to build something it can't figure out alone, like which class to use for an interface.
From plain DI to the container
We'll use a blog where publishing a post creates a slug and notifies subscribers.
-
Dependency injection, without any container
First, the bad version: the class builds its own helpers with
new. It's glued to those exact classes, and you can't swap them in a test.class PostPublisher { public function publish(Post $post): void { $slugs = new SlugGenerator(); // hard-wired $notifier = new MailNotifier(); // hard-wired // ... } }Now the DI version. The class simply asks for what it needs in its constructor. Whoever creates it must supply them:
class PostPublisher { public function __construct( private SlugGenerator $slugs, private Notifier $notifier, ) {} public function publish(Post $post): void { $post->slug = $this->slugs->for($post->title); $post->save(); $this->notifier->send("New post: {$post->title}"); } } // Building it by hand: works, but tedious $publisher = new PostPublisher(new SlugGenerator(), new MailNotifier());That last line is the problem DI creates: somebody has to wire everything together. That somebody is the service container.
-
Let the container resolve it automatically
Type-hint a class in a controller, and Laravel builds it for you. This is called automatic resolution (or "autowiring"). The container uses PHP's reflection to read the constructor, then resolves each type-hinted parameter, recursively.
class PostController extends Controller { public function __construct(private PostPublisher $publisher) {} // Method injection works too: Laravel fills type-hinted arguments // and route parameters side by side. public function publish(Request $request, Post $post) { $this->publisher->publish($post); return back()->with('status', 'Published!'); } }Concrete classes with no special setup need zero configuration. You don't register
SlugGeneratoranywhere; the container just doesnew SlugGenerator().โน๏ธ Where does injection happen?Anything Laravel builds for you gets autowiring: controllers, middleware, listeners, jobs'
handle()methods, form requests, Artisan commands and more. -
Bind an interface to an implementation
Notifieris an interface, so the container can't guess which class to build. You register a binding in a service provider'sregister()method:use App\Contracts\Notifier; use App\Services\MailNotifier; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind(Notifier::class, MailNotifier::class); } }Every class that type-hints
Notifiernow receives aMailNotifier. Tomorrow you switch toSlackNotifierby editing this one line. Nothing else changes. That's polymorphism paying off.Need custom setup? Pass a closure. It receives the container, so you can resolve other things inside it:
$this->app->bind(Notifier::class, function (Application $app) { return new SlackNotifier( webhook: config('services.slack.webhook'), http: $app->make(HttpClient::class), ); }); -
bind vs singleton vs scoped
These three methods differ in how many instances the container hands out:
Method What you get Good for bind()A new object every time it's resolved Lightweight, stateful helpers singleton()Built once, then the same object for the rest of the app's life Expensive clients (API SDKs, connections) scoped()Once per request or queued job, then flushed Per-request state under Octane or queue workers instance()The exact object you already created Registering a pre-built object $this->app->singleton(StripeClient::class, fn () => new StripeClient(config('services.stripe.secret')) ); $this->app->scoped(CurrentTeam::class);โ ๏ธ Singletons and long-running workersUnder Laravel Octane or a queue worker, the app stays in memory between requests. A singleton holding "the current user" would leak into the next request. Use
scoped()for anything request-specific. -
Resolve things manually
Sometimes you're not in a place with injection (a plain helper function, a Tinker session). Ask the container directly. All of these do the same thing:
$publisher = app(PostPublisher::class); $publisher = app()->make(PostPublisher::class); $publisher = resolve(PostPublisher::class); // Pass extra constructor values the container can't guess: $report = app()->makeWith(PostReport::class, ['year' => 2025]);Prefer constructor injection when you can: it shows a class's dependencies right in its signature. Manual resolving hides them.
-
Contextual binding (briefly)
Occasionally two classes need different implementations of the same interface. Contextual binding says "when building X, give it Y":
$this->app->when(AdminAlertController::class) ->needs(Notifier::class) ->give(SlackNotifier::class); $this->app->when(NewsletterController::class) ->needs(Notifier::class) ->give(MailNotifier::class);You won't need this often, but it's good to know it exists before you reach for messy
ifstatements.
Avoid this, do that
public function store(Request $request)
{
$gateway = new StripeGateway('sk_live_...');
$gateway->charge(...);
}
Creating dependencies with new inside methods hard-wires them and makes the code impossible to test without real charges.
public function store(Request $request, PaymentGateway $gateway)
{
$gateway->charge(...);
}
Type-hint an interface and bind it in a provider. Tests can then swap in a fake gateway.
public function register(): void
{
$this->app->bind(SlugGenerator::class, SlugGenerator::class);
}
Binding a concrete class to itself does nothing useful; the container already builds it automatically.
public function register(): void
{
$this->app->bind(Notifier::class, MailNotifier::class);
}
Only bind what needs a decision: interfaces, config values or one-instance rules.
In a test you can replace any binding before the code runs. The rest of the app receives your fake without knowing it:
$this->app->instance(Notifier::class, new FakeNotifier());
- Create
app/Services/SlugGenerator.phpwith afor(string $title): stringmethod that callsStr::slug(). - Type-hint it in a controller method and return its output. Notice you registered nothing.
- Create a
Notifierinterface and two classes,LogNotifierandMailNotifier. Bind the interface toLogNotifierinAppServiceProvider. - In
php artisan tinker, compareapp(Notifier::class) === app(Notifier::class)withbind(), then withsingleton().
Key takeaways
- Dependency injection means a class receives what it needs instead of creating it.
- The container autowires concrete classes by reading type-hints; no setup needed.
- Bind interfaces to implementations in a service provider's
register(). bind()= new each time,singleton()= one forever,scoped()= one per request/job.app(),app()->make()andresolve()resolve manually; prefer constructor injection.- Contextual binding gives different classes different implementations of one interface.