You’ve written this constructor a hundred times:
class OrderController
{
public function __construct(
private OrderService $orderService
) {}
}
You never instantiate OrderController yourself. You never write new OrderService(...) anywhere near it. Yet when a request hits this controller, $orderService is there, fully built, dependencies and all.
Most Laravel developers accept this as “how Laravel works” and move on. That’s fine until the day it isn’t — until you bind an interface, get a BindingResolutionException, and have no idea why the thing that “just worked” everywhere else suddenly doesn’t. Or until an interview asks you to explain, precisely, what happens between Route::get(...) and your controller method running.
This article answers one question in full: when you type-hint a service in a Laravel class, how does Laravel know what object to create, where does that knowledge come from, and what happens when that object needs its own dependencies?
By the end, you’ll be able to trace that chain all the way down — not as a metaphor, but as an actual sequence of operations.
The Problem: Who Builds Your Dependencies?
Start with plain PHP, no framework involved.
class OrderService
{
private StripePaymentGateway $paymentGateway;
public function __construct()
{
$this->paymentGateway = new StripePaymentGateway();
}
}
This works. It also has three problems that get worse as the codebase grows.
It’s tightly coupled. OrderService doesn’t just use Stripe — it is welded to it. Swapping in PayPal, or a fake gateway for testing, means editing OrderService itself.
It’s hard to test. Any test that instantiates OrderService also instantiates StripePaymentGateway, which likely means real HTTP calls, real API keys, or elaborate mocking hacks just to get past the constructor.
It doesn’t scale. Real services don’t depend on one thing. They depend on repositories, loggers, HTTP clients, config objects. If every class builds its own dependencies, and those dependencies build their dependencies, you get a tree of new calls buried at every layer, each layer secretly aware of implementation details it shouldn’t care about.
The object shouldn’t be responsible for building what it depends on. It should just declare what it needs.
Dependency Injection
A dependency is anything a class needs from the outside to do its job — a payment gateway, a repository, a logger, an HTTP client.
Dependency injection is the practice of handing a class its dependencies from the outside, rather than letting the class construct them itself. The most common form in PHP is constructor injection:
class OrderService
{
public function __construct(
private StripePaymentGateway $paymentGateway
) {}
}
Now OrderService receives its gateway instead of manufacturing it. Whoever creates an OrderService decides which gateway it gets:
$service = new OrderService(new StripePaymentGateway());
This alone fixes testability — a test can now pass in a fake gateway. But it still couples OrderService to one concrete class. The real fix is to depend on an interface, not an implementation:
interface PaymentGateway
{
public function charge(float $amount): bool;
}
class StripePaymentGateway implements PaymentGateway
{
public function charge(float $amount): bool
{
// Call the Stripe API
return true;
}
}
class OrderService
{
public function __construct(
private PaymentGateway $paymentGateway
) {}
}
OrderService now knows only that it has something capable of charging money. It doesn’t know or care whether that’s Stripe, PayPal, or a test double. This is the whole point of interface-based dependency injection: the dependent class is decoupled from the decision of which implementation it gets.
But this raises the exact question this article exists to answer:
If
OrderServiceasks for aPaymentGateway— an interface, which cannot be instantiated — how does Laravel know to hand it aStripePaymentGateway?
That question is answered by the Service Container.
What Is Laravel’s Service Container?
Here’s the mental model to hold onto: the Service Container is Laravel’s object factory and dependency manager. It has two jobs — remembering how to build things (binding), and actually building them on request (resolving).
Every Laravel application has exactly one container instance, accessible as $this->app inside service providers, or via the app() helper elsewhere.
Binding
A binding tells the container how to produce something when it’s asked for. You register bindings by hand for the cases the container can’t figure out on its own — mainly interfaces:
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
This says: “whenever something needs a PaymentGateway, build a StripePaymentGateway.”
Resolving
Resolving is asking the container to produce an object:
$gateway = $this->app->make(PaymentGateway::class);
The container looks up the binding, sees StripePaymentGateway, and builds one.
Bind vs. Singleton
bind() produces a new instance every time it’s resolved. singleton() builds the object once and returns the same instance on every subsequent resolution:
$this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);
We’ll come back to this distinction with a full comparison later — it matters more than it looks.
Automatic Resolution
Here’s the part that surprises people: most of the time, you don’t bind anything at all. If you ask the container for a concrete class with no unresolvable dependencies, it can build it without being told how:
class ReportRepository
{
public function __construct(private DatabaseConnection $connection) {}
}
$repository = $this->app->make(ReportRepository::class);
No binding was registered for ReportRepository. The container built it anyway, by inspecting its constructor. This is the mechanism the next section explains in detail.
app() Helper vs. $this->app->make()
Functionally, these are equivalent:
app(MyService::class);
$this->app->make(MyService::class);
app() is a global helper that returns the container instance (or resolves a class if you pass one), while $this->app is the container instance available inside classes that already have access to it, like service providers. Outside of a service provider, app() is simply the more convenient way to reach the same container.
When Should You Manually Resolve?
Almost never, inside your own application code. The right pattern in Laravel is to type-hint dependencies in constructors and let the container inject them automatically — that’s what the rest of this article is about. Reaching for app()->make() inside a controller or service is usually a sign you should be injecting instead.
Legitimate uses of manual resolution are narrower: inside service provider register()/boot() methods where you’re wiring things up, in bootstrapping code, or in rare cases where the class you need depends on a runtime value you only have at that exact moment (like a dynamically-chosen class name).
How Laravel Automatically Builds Objects
This is the mechanism that makes the rest of Laravel feel automatic, and it deserves a precise explanation rather than a hand-wave.
Take this chain:
class OrderController
{
public function __construct(
private OrderService $orderService
) {}
}
class OrderService
{
public function __construct(
private PaymentService $paymentService
) {}
}
class PaymentService
{
public function __construct(
private PaymentGateway $paymentGateway
) {}
}
OrderController
↓ needs
OrderService
↓ needs
PaymentService
↓ needs
PaymentGateway (interface)
↓ resolved via binding to
StripePaymentGateway
When the container is asked to resolve OrderController, here’s what actually happens, step by step:
- The container checks for an explicit binding for
OrderController. There isn’t one — it’s a plain concrete class. - It uses PHP’s Reflection API (
ReflectionClass) to inspectOrderController’s constructor. Reflection lets the container examine a class’s structure at runtime — its constructor parameters, their type hints, whether they have default values — without the container needing any prior knowledge of that class. - It reads the constructor’s parameter list and sees one type-hinted parameter:
OrderService. - It recursively resolves
OrderServicethe same way: check for a binding (none), reflect its constructor, findPaymentService. - It recursively resolves
PaymentService: check for a binding (none), reflect its constructor, findPaymentGateway. - It tries to resolve
PaymentGateway— but this is an interface. Reflection can tell the container thatPaymentGatewayis required, but it cannot instantiate an interface. At this point the container must consult its bindings. IfPaymentGateway::class => StripePaymentGateway::classwas registered, the container resolvesStripePaymentGatewayinstead — recursing into its constructor as well. - Each object is built bottom-up:
StripePaymentGatewayfirst, thenPaymentService(with the gateway injected), thenOrderService(with the payment service injected), thenOrderController(with the order service injected).
This is why it’s called recursive dependency resolution — the container doesn’t resolve a flat list, it walks a tree, resolving leaves before the branches that depend on them.
When Laravel Can Resolve Automatically vs. When It Can’t
- A concrete class with resolvable constructor parameters — Laravel can build it via reflection, no binding needed.
- An interface or abstract class — reflection can identify that one is required, but not which implementation to use. A binding is required.
- A primitive value (
string,int,bool,array) — reflection can see a parameter exists, but there’s no type to resolve from; the container has no way to guess what value you mean. This needs explicit configuration, covered later.
It’s worth being precise here: this reflection-based process happens every time the container needs to build something it doesn’t already have bindings or a resolved singleton for — it’s not a one-time startup scan of your whole application. Laravel doesn’t “know” your class graph in advance; it discovers it on demand, one make() call at a time.
Interface Bindings
Go back to the point where resolution must stop and ask for help:
class OrderService
{
public function __construct(
private PaymentGateway $paymentGateway
) {}
}
PaymentGateway is an interface. There is no way to do new PaymentGateway() — PHP won’t allow it, and reflection can’t manufacture an implementation out of thin air. The container needs to be told, explicitly, which concrete class satisfies this interface:
$this->app->bind(
PaymentGateway::class,
StripePaymentGateway::class
);
Read literally: “whenever something asks for PaymentGateway, give it a StripePaymentGateway.” Once this binding exists, every class in the dependency tree that type-hints PaymentGateway transparently receives a StripePaymentGateway — including deeply nested ones, as in the chain above.
Closure Bindings
Sometimes building the concrete class isn’t just a matter of new ClassName() — it needs specific construction logic:
$this->app->bind(PaymentGateway::class, function ($app) {
return new StripePaymentGateway(
$app->make(HttpClient::class)
);
});
The closure receives the container itself ($app), so it can pull in other dependencies, read config, or apply conditional logic before returning the object. Use a closure binding when construction needs to be more than “make an instance of this other class” — for example, passing in a config value, choosing between implementations based on environment, or performing setup the constructor alone can’t express.
Service Providers
Bindings have to be registered somewhere, before anything tries to resolve them. That “somewhere” is a Service Provider.
Service Providers are where Laravel applications register and configure services with the container. Every piece of built-in Laravel functionality — the database layer, the queue system, routing, authentication — is bootstrapped by a service provider. Your own bindings belong in providers too, following the same pattern the framework uses internally.
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
PaymentGateway::class,
StripePaymentGateway::class
);
}
public function boot(): void
{
//
}
}
Providers are registered in bootstrap/providers.php (Laravel 11+) or config/app.php’s providers array (earlier versions), and Laravel instantiates and runs them during application startup — before any route is dispatched.
register() vs. boot()
These two methods look similar but exist for different, non-overlapping purposes, and mixing them up causes real bugs.
register() |
boot() |
|
|---|---|---|
| Purpose | Register bindings into the container | Run initialization logic once everything is registered |
| Timing | Called on every provider, in sequence | Called on every provider, only after all providers have finished register() |
| Can it rely on other providers’ bindings existing? | No — another provider’s register() may not have run yet |
Yes — every provider’s bindings are in place by the time any boot() runs |
| Typical contents | $this->app->bind(...), $this->app->singleton(...) |
View composers, event listener registration, calling Gate::define(), resolving services to configure them |
Why the split matters: if PaymentServiceProvider::register() tried to resolve a binding registered by a different provider, it might fail — that other provider’s register() hasn’t necessarily run yet, since provider load order isn’t something you should depend on. register() is for declaring what the container knows; it should be side-effect-free and not assume any other service already exists.
boot(), by contrast, runs only after every provider has had its turn to register. This is the safe place to do things that depend on the rest of the application being wired up:
public function boot(): void
{
Gate::define('manage-orders', function (User $user) {
return $user->isAdmin();
});
}
Rule of thumb: if you’re telling the container how to build something, that’s register(). If you’re using already-built services to configure behavior, that’s boot().
The Relationship Between Container and Provider
This is the idea the rest of the article hangs on, so it’s worth stating as plainly as possible:
Service Providers teach the container how to build things. The container is what actually builds them.
Service Provider
↓
Registers a binding
↓
Service Container
↓
Stores the knowledge: "PaymentGateway → StripePaymentGateway"
↓
Application code requests PaymentGateway (directly, or via a constructor)
↓
Container checks its bindings, finds the match
↓
Container resolves StripePaymentGateway (reflecting its own constructor if needed)
↓
Object is created and handed back
The provider does its work exactly once, at boot time. The container does its work on every single resolution, for the entire lifetime of the request (or application, for singletons). Providers are configuration; the container is the runtime engine that acts on that configuration.
Bind vs. Singleton
Both register bindings. The difference is entirely about object lifecycle.
bind() |
singleton() |
|
|---|---|---|
| Instances created | A new instance every time the type is resolved | One instance, reused for every resolution after the first |
| When a new object appears | Every make() call, every injection |
Never, after the first resolution |
| Typical use case | Stateless services, objects that shouldn’t share state across uses | Shared state, expensive-to-construct objects, things that should behave as one shared instance |
| Example scenario | A PaymentGateway implementation with no internal state — fine to recreate |
A Logger writing to one open file handle, or a Cache connection you don’t want to reconnect on every use |
// A new StripePaymentGateway on every resolution
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
// The same instance every time, for the life of the request
$this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);
One nuance worth being exact about: a singleton is one instance per application lifecycle — for a typical web request, that means per request (each new HTTP request boots a fresh application container, in the standard PHP-FPM/Apache model). It is not “one instance forever, across all users and all time,” which is a common misreading of the term. Under long-running process models (Octane, queue workers), the lifetime of a singleton extends to the lifetime of that worker process instead — which is exactly why holding request-specific state in a singleton under those setups is a real footgun, not a theoretical one.
Contextual Binding
Sometimes two different classes need different implementations of the same interface. A single global binding can’t express that — this is what contextual binding solves.
$this->app
->when(PayPalPaymentService::class)
->needs(PaymentGateway::class)
->give(PayPalPaymentGateway::class);
$this->app
->when(StripePaymentService::class)
->needs(PaymentGateway::class)
->give(StripePaymentGateway::class);
Read it as: “when the container is building a PayPalPaymentService, and it needs a PaymentGateway, give it PayPalPaymentGateway specifically — regardless of what the default binding says.”
This is useful whenever the “right” implementation of an interface depends on who’s asking, not just on the interface itself — for example, different queue drivers needing different clients, or different notification channels needing different HTTP clients, even though they all depend on the same interface.
Primitive Dependencies
Reflection breaks down completely for primitives:
class PaymentService
{
public function __construct(
private string $apiKey
) {}
}
The container can see that PaymentService needs a string. It has no way to know which string — "sk_test_123" isn’t information reflection can extract from a type hint, because a type hint of string carries no semantic meaning about its content. Resolving this automatically would require the container to guess, and it doesn’t guess.
There are a few correct ways to handle this, and directly sprinkling env() through application classes is not one of them — env() is meant to be read only inside config files, so config caching (php artisan config:cache) works correctly.
Configuration file, read at the point of use:
// config/services.php
return [
'stripe' => [
'key' => env('STRIPE_API_KEY'),
],
];
A closure binding that pulls from config:
$this->app->bind(PaymentService::class, function ($app) {
return new PaymentService(
config('services.stripe.key')
);
});
Named primitive binding via give(), for cases where several classes need the same primitive:
$this->app
->when(PaymentService::class)
->needs('$apiKey')
->give(fn () => config('services.stripe.key'));
All three approaches share the same principle: primitives are configured explicitly, not inferred.
Real Request Lifecycle
Here’s the request lifecycle, with the container’s involvement made explicit rather than implied:
HTTP Request
↓
Route matched
↓
Route resolves its controller — via the container
↓
Container reflects the controller's constructor
↓
Container resolves each constructor dependency (recursively, as covered above)
↓
Container calls the matched controller method,
also resolving any method-injected dependencies (e.g. Form Requests)
↓
Controller method runs business logic using its injected services
↓
Response returned
The container isn’t a side character here — it’s the mechanism that turns a route definition into an actual, fully-wired controller instance, and it participates again for every dependency, every layer deep.
Where Laravel Uses Dependency Injection
The container isn’t limited to controllers. It’s the mechanism behind dependency resolution across most framework-managed classes:
// Middleware
class EnsureUserIsActive
{
public function __construct(private UserRepository $users) {}
public function handle($request, Closure $next) { /* ... */ }
}
// Queued jobs
class ProcessPayment implements ShouldQueue
{
public function handle(PaymentService $paymentService)
{
// Resolved when the job is executed, not when it's dispatched
}
}
// Event listeners
class SendOrderConfirmation
{
public function __construct(private Mailer $mailer) {}
public function handle(OrderPlaced $event) { /* ... */ }
}
// Artisan commands
class SyncInventory extends Command
{
public function handle(InventoryService $inventory) { /* ... */ }
}
// Form Requests
class StoreOrderRequest extends FormRequest
{
public function authorize(Gate $gate): bool { /* ... */ }
}
// Route closures
Route::get('/orders', function (OrderRepository $orders) {
return $orders->all();
});
In every one of these, the same underlying mechanism applies: reflect the constructor or method signature, resolve each type-hinted parameter recursively, consult bindings for interfaces, inject the result. The container is genuinely everywhere in the framework — this isn’t a controller-only trick.
Facades, Briefly
Facades often get confused with the container itself, so it’s worth a short, precise clarification.
A facade like Cache::get('key') looks like a static method call, but it isn’t one. Cache is a thin class that, under the hood, resolves the real underlying service (in this case, the cache manager) out of the container and forwards the call to it. Every facade extends a base Facade class whose __callStatic magic method resolves the real service by a string key (getFacadeAccessor()) and calls the method on that resolved object.
So Cache::get('key') is really, approximately:
app('cache')->get('key');
Facades are a convenience layer on top of the container, not an alternative to it. The service being called is still a container-managed object — usually a singleton — with all the same resolution rules discussed above. They’re mentioned here only to prevent the common mistake of thinking facades bypass dependency injection; they don’t, they just hide it behind a static-looking syntax.
Complete Practical Example: Payment Gateway
Here’s the full picture, assembled as one coherent example, from interface to request.
1. The interface:
interface PaymentGateway
{
public function charge(float $amount): bool;
}
2. The concrete implementation:
class StripePaymentGateway implements PaymentGateway
{
public function __construct(private string $apiKey) {}
public function charge(float $amount): bool
{
// Call Stripe's API using $this->apiKey
return true;
}
}
3. The service that depends on the interface:
class PaymentService
{
public function __construct(private PaymentGateway $gateway) {}
public function process(float $amount): bool
{
return $this->gateway->charge($amount);
}
}
class OrderService
{
public function __construct(private PaymentService $paymentService) {}
public function placeOrder(float $total): bool
{
return $this->paymentService->process($total);
}
}
4 & 5. The binding, inside a service provider:
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(PaymentGateway::class, function ($app) {
return new StripePaymentGateway(
config('services.stripe.key')
);
});
}
}
6. The controller:
class OrderController
{
public function __construct(private OrderService $orderService) {}
public function store(Request $request)
{
$success = $this->orderService->placeOrder(
$request->input('amount')
);
return response()->json(['success' => $success]);
}
}
7. What happens on a request to this controller:
- The router matches the request to
OrderController::store. - The container is asked to resolve
OrderController. No binding exists for it, so the container reflects its constructor and findsOrderService. - Resolving
OrderServicemeans reflecting its constructor and findingPaymentService. - Resolving
PaymentServicemeans reflecting its constructor and findingPaymentGateway— an interface. - The container checks its bindings, finds the closure registered in
PaymentServiceProvider, and executes it — which reads the API key from config and returns aStripePaymentGateway. - That
StripePaymentGatewayis injected into a newPaymentService, which is injected into a newOrderService, which is injected into a newOrderController. - The controller method runs, using services that were fully built without a single explicit
newcall in application code.
Testing With the Service Container
This entire mechanism pays off directly in tests. Since OrderService depends on the PaymentGateway interface — not a concrete class — a test can swap in a fake implementation by rebinding it in the container:
class FakePaymentGateway implements PaymentGateway
{
public function charge(float $amount): bool
{
return true;
}
}
public function test_order_can_be_placed_without_hitting_stripe(): void
{
$this->app->bind(PaymentGateway::class, FakePaymentGateway::class);
$response = $this->postJson('/orders', ['amount' => 49.99]);
$response->assertOk()->assertJson(['success' => true]);
}
Nothing about OrderController, OrderService, or PaymentService changed. The container simply resolved a different implementation for the same interface, because the test rebound it before the request ran. This is the entire payoff of interface-based dependency injection made concrete: production code stays untouched, and tests run without ever touching a real payment provider.
Common Misconceptions About Laravel’s Service Container
“The Service Container is just an array.” It stores bindings in an internal structure, but it also performs reflection-based resolution, manages singleton instances, handles contextual bindings, and resolves dependency trees recursively. An array can’t do any of that on its own — the container is the logic around the data, not the data itself.
“Every class must be registered manually.” Only interfaces, abstract classes, and primitives generally need explicit bindings. Concrete classes with resolvable constructors are built automatically via reflection, with no registration at all.
“Service Providers create every object.” Providers configure the container — they register bindings and run boot-time setup. The container is what actually instantiates objects, and it does so lazily, on demand, not when the provider runs.
“Facades are the same thing as the Service Container.” Facades are a thin static-style interface that resolves an object from the container and delegates to it. They use the container; they aren’t the container.
“Singleton means one instance for the entire world.” A singleton is one instance per application lifecycle — typically one per HTTP request under the standard PHP request model, not one instance shared across every request and every user forever.
“Laravel magically knows everything.” It doesn’t know anything in advance. It discovers a class’s dependencies by reflecting its constructor at the moment it’s asked to build that class, and it falls back to registered bindings only when reflection alone isn’t enough — interfaces, abstract classes, and primitives.
“Dependency injection only works in controllers.” The container resolves dependencies for middleware, jobs, event listeners, Artisan commands, form requests, and route closures — anywhere Laravel is responsible for instantiating the class.
A Simple Mental Model
Strip away every implementation detail and this is the process, at a conceptual level:
1. Laravel needs to build a class (a controller, a job, a resolved dependency, etc.)
2. The container checks: is there an explicit binding for this?
3. If yes, and it's a singleton already resolved, return the existing instance.
4. If no binding exists, and it's a concrete class, reflect its constructor.
5. For each constructor parameter, resolve it — recursively, using this same process.
6. If a parameter is an interface or abstract class, look for a binding.
No binding, no automatic instantiation — this is where resolution fails
if nothing was registered.
7. If a parameter is a primitive with no explicit value provided, resolution
fails unless a binding or default value supplies one.
8. Once every parameter is resolved, construct the object and return it.
9. The caller (which may itself be mid-construction, one level up)
receives the fully-built object as its own dependency.
This is a simplified conceptual model, not a line-by-line account of Laravel’s actual Container class — the real implementation includes caching of reflection data, handling of variadic parameters, method injection separate from constructor injection, and various edge cases around building contextual bindings. But this model is accurate to how the process behaves, and it’s enough to reason correctly about any binding problem you’ll hit in practice.
Conclusion
The question this article set out to answer was never really “what is the Service Container” — it was “how does Laravel figure out what to build.” The answer, in full: Laravel doesn’t guess. Service Providers register the pieces of knowledge the container can’t derive on its own — which concrete class satisfies an interface, which value fills a primitive parameter, which implementation a specific class needs contextually. The container then uses that knowledge, plus reflection over everything it can figure out automatically, to walk a class’s entire dependency tree and build it from the bottom up.
Once you can trace that path — from a type-hinted constructor parameter, through reflection, through binding lookups, down to the concrete object handed back — the rest of Laravel stops looking like magic and starts looking like what it is: a consistent, inspectable system that happens to be running quietly under almost everything you write.