What is the Service Container?
The Laravel Service Container is a smart registry and factory at the core of every
Laravel application. It does three things:
Stores HOW to build classes — you register bindings that describe how a class or interface should be constructed
Builds them automatically — when you need a class, the container resolves it along with all its dependencies, recursively
Manages lifetimes — you control whether you get a fresh instance every time (transient), a shared instance per request (singleton), or one that resets between jobs (scoped)
Two foundational software patterns are baked into the container:
Dependency Injection (DI)
→ Pass dependencies INTO a class instead of creating them inside it.
Makes code testable, decoupled, and swappable.
Inversion of Control (IoC)
→ The framework controls HOW objects are built, not your code.
You just ask for what you need — it delivers.
You can access the container from anywhere:
$this->app // inside Service Providers
App::make(Foo::class) // via Facade (anywhere)
app(Foo::class) // via helper (anywhere)
// type-hint in constructor / method ← PREFERRED in 99% of cases
The Problem It Solves
To understand why the container exists, consider what you'd have to do without it.
Without the Container – Tight Coupling
<?php
class PodcastController
{
public function __construct()
{
// You manually build EVERY dependency
$parser = new PodcastParser();
$this->apple = new AppleMusic($parser);
}
}
Problems: tightly coupled, hard to swap implementations, impossible to test in isolation.
Every change to AppleMusic's constructor breaks this code.
With the Container – Loose Coupling
<?php
class PodcastController
{
public function __construct(
protected AppleMusic $apple, // ← just declare what you need
) {}
}
Laravel builds AppleMusic — and its own dependencies — for you.
You can swap implementations, mock them in tests, or change their construction
without touching PodcastController at all.
Zero Configuration Resolution
If a class has no interface dependencies (only concrete classes),
the container resolves it automatically using PHP Reflection. No binding required.
// No binding registered — works automatically
Route::get('/', function (Service $service) {
dd($service::class); // Laravel builds Service and injects it
});
The rule of thumb:
Concrete class → no binding needed, container auto-resolves via reflection
Interface → MUST manually bind interface → implementation
All Binding Methods
All bindings are registered inside a Service Provider's register() method, using $this->app.
1. bind() — New Instance Every Time (Transient)
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});
// Every call to make(Transistor::class) returns a fresh new object.
2. singleton() — Same Instance Every Time
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});
// First call builds it. All future calls return the SAME object.
// In PHP-FPM: "per request" — container is rebuilt each request.
// In Octane: truly persistent across requests — be careful with state.
3. scoped() — Singleton, but Resets Per Request/Job
$this->app->scoped(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});
// Like singleton, but explicitly resets when:
// • Octane starts a new request
// • Queue worker processes a new job
// Use scoped() instead of singleton() when using Octane or queues.
4. instance() — Bind a Pre-Built Object
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
// The EXACT object you pass will always be returned.
5. bindIf / singletonIf / scopedIf — Conditional Bindings
// Only registers if that type hasn't been bound yet.
// Useful in packages — allows users to override your default binding.
$this->app->singletonIf(PaymentGateway::class, fn() => new StripeGateway);
Comparison Table
Method New instance each call? Resets on new lifecycle?
────────── ───────────────────────── ────────────────────────
bind() YES N/A
singleton() NO (cached forever*) NO (* per PHP process)
scoped() NO (cached per lifecycle) YES
instance() NO (fixed object) NO
Binding Interfaces to Implementations
This is the most powerful use of the container. By binding an interface to an implementation,
your application code depends only on contracts — never on concrete classes.
<?php
// 1. Define the contract (interface)
interface EventPusher
{
public function push(string $event, array $data): void;
}
// 2. Create the implementation
class RedisEventPusher implements EventPusher
{
public function push(string $event, array $data): void
{
// push to Redis...
}
}
// 3. Register in a Service Provider
$this->app->bind(EventPusher::class, RedisEventPusher::class);
// 4. Use it anywhere — type-hint the INTERFACE, not the class
class NotificationController extends Controller
{
public function __construct(protected EventPusher $pusher) {}
}
// 5. Swap in tests with zero app changes
$this->app->bind(EventPusher::class, FakeEventPusher::class);
⚠️ This is the Open/Closed Principle in action. Your application code is open for extension (new implementations) but closed for modification — you never touch the controller to swap the implementation.
PHP Attributes – Modern Binding Syntax
Laravel supports PHP 8 Attributes as an alternative to manual binding calls in service providers.
You declare the binding intent directly on the class or interface itself.
#[Singleton] — Resolve Only Once
use Illuminate\Container\Attributes\Singleton;
#[Singleton]
class Transistor { ... }
#[Scoped] — Singleton Per Request/Job
use Illuminate\Container\Attributes\Scoped;
#[Scoped]
class Transistor { ... }
#[Bind] — Bind Interface to Implementation
use Illuminate\Container\Attributes\Bind;
// Bind to one implementation
#[Bind(RedisEventPusher::class)]
interface EventPusher { ... }
// Different implementations per environment
#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher { ... }
// Combined — singleton AND environment-specific binding
#[Bind(RedisEventPusher::class)]
#[Singleton]
interface EventPusher { ... }
No service provider changes needed. The intent is declared right where it belongs —
at the interface definition.
Contextual Binding
Sometimes two different classes need different implementations of the same interface.
Contextual binding solves this cleanly.
// Problem: PhotoController needs local disk. VideoController needs S3.
// Both type-hint the same Filesystem interface.
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(fn() => Storage::disk('local'));
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(fn() => Storage::disk('s3'));
Inject Primitive Values
// Inject a plain value
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);
// Inject a config value
$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');
// Inject all tagged bindings
$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');
Contextual Attributes
Instead of writing contextual binding in a provider, use PHP attributes directly on constructor
parameters. The intent is visible right at the point of use — no jumping between files.
<?php
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $filesystem,
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[Context('uuid')] protected string $uuid,
#[DB('mysql')] protected Connection $connection,
#[Give(DatabaseRepository::class)] protected UserRepository $users,
#[Log('daily')] protected LoggerInterface $log,
#[RouteParameter('photo')] protected Photo $photo,
#[Tag('reports')] protected iterable $reports,
#[CurrentUser] protected User $user,
) {}
}
Custom Contextual Attribute
#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
public function __construct(
public string $key,
public mixed $default = null
) {}
public static function resolve(self $attribute, Container $container): mixed
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}
// Usage:
#[Config('app.name')] protected string $appName
Tagging – Group Related Bindings
Tags let you group multiple bindings under a shared label and resolve all of them at once.
Perfect for plugin systems, pipeline stages, or report generators.
// Register several implementations
$this->app->bind(CpuReport::class, fn() => new CpuReport);
$this->app->bind(MemoryReport::class, fn() => new MemoryReport);
// Tag them as a group
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
// Resolve ALL tagged bindings at once
$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
// ↑ returns array of all tagged instances
});
Extending Bindings (Decorator Pattern)
extend() lets you wrap or modify a service after it's been built — without
touching the original class. This is the Decorator pattern applied to the container.
$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});
// Use case: add logging, caching, or monitoring wrappers
// around a service without modifying its source code.
Resolving from the Container
Multiple ways to get instances out of the container — each suited for different contexts:
Method When to use
───────────────────────────── ──────────────────────────────────────────────
Type-hint in constructor 99% of cases. Preferred. Fully automatic.
$this->app->make(Foo::class) Inside service providers / closures
App::make(Foo::class) Anywhere via Facade
app(Foo::class) Anywhere via helper function
$this->app->makeWith( When you need to pass extra args at resolve time
Foo::class, ['id' => 1])
$this->app->bound(Foo::class) Check if a binding exists before resolving
Inject the Container Itself
public function __construct(protected Container $container) {}
// Or PSR-11 compatible:
public function __construct(protected ContainerInterface $container) {}
Method Injection
// Invoke a method with auto-injected dependencies
App::call([new PodcastStats, 'generate']);
// Or a closure
App::call(function (AppleMusic $apple) {
// $apple is auto-injected from the container
});
Container Events
The container fires events when objects are resolved. You can hook in to configure
or decorate services right after they're built.
// Listen for every resolved object
$this->app->resolving(function (mixed $object, Application $app) {
// runs for EVERY resolved object
});
// Listen for a specific class
$this->app->resolving(Transistor::class, function (Transistor $t, Application $app) {
$t->setLogger(app(Logger::class)); // configure the object before it's used
});
// rebinding() — fires when a binding is REPLACED
// Use case: if service A holds a reference to B, and B gets re-bound,
// rebinding lets A update its reference automatically.
$this->app->rebinding(PodcastPublisher::class, function ($app, $newInstance) {
// update cached references to the old instance
});
PSR-11 Compatibility
Laravel's container implements the PSR-11 ContainerInterface standard.
Type-hint ContainerInterface for framework-agnostic code:
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
});
// PSR-11 exceptions:
// NotFoundExceptionInterface → identifier never bound
// ContainerExceptionInterface → bound but couldn't resolve
Pro Developer Insights
1. Prefer type-hinting over manual make() calls
Let Laravel inject automatically. Only use app()->make() inside closures or factories
that don't support auto-injection — never in controllers or services.
2. Bind interfaces, not concrete classes
Your application code should depend on contracts (interfaces), not implementations.
This is the Open/Closed Principle — your app is extensible without modification.
3. singleton() vs scoped() in long-running apps
In standard PHP-FPM both behave identically (container rebuilds per request).
In Octane or queue workers, use scoped() for request-bound state
to avoid data leaking between requests.
4. Don't over-bind
No need to bind a class just because it exists. Only bind when:
You need to map an interface to an implementation
You need custom constructor logic or config values
You need a specific lifetime (singleton/scoped)
5. Use #[Bind] + #[Singleton] attributes for cleaner code
Modern Laravel lets you declare binding intent at the source — on the interface or class itself —
rather than buried deep inside a service provider.
6. Use tagging for "collection of implementations" patterns
Report analyzers, pipeline stages, event processors — any time you have
"multiple things of the same type", tagging is the elegant solution.
7. extend() is the Decorator pattern
Add cross-cutting concerns (logging, caching, metrics) to any resolved service
without modifying its source class.
8. The container rebuilds on every PHP-FPM request
Singletons are only singletons within one request. In Octane they persist across
requests — design accordingly and prefer scoped() for stateful services.
Quick Reference – All Methods
BINDING
────────
bind(abstract, closure) New instance every resolve
singleton(abstract, closure) One instance, cached (per process)
scoped(abstract, closure) One instance, reset per lifecycle
instance(abstract, $object) Bind a pre-built object
bindIf / singletonIf / scopedIf Conditional (only if not already bound)
CONTEXTUAL
───────────
when(Class)->needs(X)->give(Y) Different Y for different classes
->give($primitiveValue) Inject plain values
->giveConfig('key') Inject config value
->giveTagged('tag') Inject all tagged bindings
TAGGING
────────
tag([A::class, B::class], 'name') Assign a group tag
tagged('name') Resolve all in group as array
EXTENDING
──────────
extend(Class, closure) Decorate/modify after resolution
RESOLVING
──────────
make(Class) Resolve from container
makeWith(Class, ['key' => val]) Resolve with manual args
bound(Class) Check if binding exists
call([obj, 'method']) Invoke method with auto-injection
tagged('tag') Resolve all tagged instances
EVENTS
───────
resolving(Class, closure) Hook into any resolution
rebinding(Class, closure) Hook into re-registration
Conclusion
The Service Container is the most important piece of Laravel's architecture.
Everything — facades, service providers, route model binding, controller injection —
flows through it.
Use type-hinting for automatic injection — it's the right choice in 99% of cases
Bind interfaces to implementations so your app stays swappable and testable
Use singleton() for shared services, scoped() in Octane/queues, bind() for fresh instances
Contextual binding handles the case where different classes need different implementations
PHP Attributes (#[Bind], #[Singleton]) let you declare intent at the source
Tagging elegantly handles groups of related bindings
extend() is the Decorator pattern — add behavior without touching the original class