Service Container & Dependency Injection
Every controller, job, and event listener you've written in this track has needed collaborator objects to do its job — a repository to fetch data, a mailer to send an email, an HTTP client to call an API. Manually constructing all of those dependencies by hand, everywhere they're needed, gets tangled fast. Laravel's service container is the piece of the framework responsible for managing class dependencies and performing dependency injection — handing a class the objects it needs, automatically, instead of it having to build them itself.
What Is the Service Container?
Think of the service container as a very capable factory that knows how to build almost any class
in your application on demand. Whenever Laravel needs to construct something — a controller to
handle a request, a job to process a queue entry, a listener to react to an event — it asks the
container for an instance, rather than you writing new SomeClass() yourself. The
container inspects what that class needs and supplies it.
Auto-Wiring: Zero Configuration Needed
The container's most powerful trick is auto-wiring: for most classes, Laravel can figure out how to build them automatically just by reading the type-hints on the constructor — with no manual setup required at all. If a class's constructor asks for another concrete class, the container recursively resolves that dependency too, all the way down.
Here's a controller depending on a plain, concrete ArticleRepository class:
<?php
namespace App\Repositories;
use App\Models\Article;
class ArticleRepository
{
public function latest(int $limit = 10): mixed
{
return Article::latest()->take($limit)->get();
}
}
<?php
namespace App\Http\Controllers;
use App\Repositories\ArticleRepository;
class ArticleController extends Controller
{
protected ArticleRepository $articles;
public function __construct(ArticleRepository $articles)
{
$this->articles = $articles;
}
public function index()
{
return view('articles.index', [
'articles' => $this->articles->latest(),
]);
}
}
You never write new ArticleRepository() anywhere. When a request hits a route pointing
at ArticleController, Laravel resolves the controller out of the container, sees the
constructor wants an ArticleRepository, builds one automatically, and hands it in — all
without a single line of configuration.
When You Need to Bind Manually
Auto-wiring works beautifully when a constructor type-hints a concrete class, because there's only one possible thing to build. It breaks down the moment a class depends on an interface instead — the container has no way to guess which concrete implementation of that interface you actually want. This is when you register a manual binding, telling the container explicitly which class to hand out whenever that interface is requested.
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
With that binding registered, any class anywhere in the application that type-hints
PaymentGateway in its constructor will receive a StripePaymentGateway
instance automatically — the calling code never needs to know or care which concrete class is
actually being used.
Where Bindings Are Registered
Bindings like the one above are almost always registered inside the register() method
of a service provider — the dedicated bootstrapping class where Laravel expects
container configuration to live. The next lesson,
Service Providers, covers exactly how that works and why
register() is the right place for it.
Singleton Bindings: bind() vs. singleton()
bind() tells the container to build a fresh instance every single time
that type is resolved. singleton() instead tells the container to build the instance
once, and hand back that exact same instance on every subsequent resolution during
the request's lifecycle — useful for things that are expensive to construct, or that need to hold
shared state across an entire request (a configuration cache, a connection object, a request-scoped
counter).
// A brand-new StripePaymentGateway every time it's resolved
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
// The SAME instance reused for every resolution
$this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);
Resolving Something Manually
Most of the time you never touch the container directly — you just type-hint a constructor and let
Laravel do the work. Occasionally, though, you need something out of the container from a place
where constructor injection isn't convenient (a route closure, a helper function, a script). The
global app() helper and the equivalent resolve() helper both pull an
instance straight out of the container:
$repository = app(ArticleRepository::class);
// Equivalent
$repository = resolve(ArticleRepository::class);
Why This Matters for Testability
Binding to an interface rather than depending directly on a concrete class isn't just an
organizational nicety — it's what makes an application genuinely testable. Because
ArticleController only knows about the PaymentGateway interface, you can
swap the real StripePaymentGateway for a fake, in-memory implementation during
automated tests, without changing a single line of the controller under test. The
Testing lesson later in this track shows exactly this technique in
practice — rebinding the container in a test so your code exercises a fake instead of hitting a real
payment provider.