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.

Where this leads: You now know how the container builds things and how to bind interfaces manually — but where do those bindings actually live in a real application? That's the job of service providers, covered next in Service Providers.