Service Container, Providers and Facades

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05; every output comes from the Bookshelf application. The container is the core of Laravel: every controller, middleware, listener, job, command and Eloquent model is created through it, so understanding it explains how dependencies "appear" in constructors. The minimal container on Templating, Routing and Middleware by Hand shows the idea in a few lines; Laravel’s version adds autowiring, lifetimes, contextual rules and events.

Autowiring

The container builds objects by reading their constructors with Reflection: for each parameter with a class or interface type it resolves that type recursively, so most classes need no registration at all. Bookshelf’s BookPresenter needs a CoverStorage (an interface) and a FineCalculator (a class):

<?php
// app/Services/BookPresenter.php
namespace App\Services;

use App\Contracts\CoverStorage;
use App\Models\Book;

class BookPresenter
{
    // Autowired: the container reads the constructor types and builds the dependencies
    public function __construct(private CoverStorage $covers, private FineCalculator $fines) {}

    public function cover(Book $book): string
    {
        return $this->covers->url($book->cover_path ?? 'placeholder.png');
    }
}

FineCalculator has no class-typed parameters, only an int read from configuration. The #[Config] contextual attribute (Laravel 11) tells the container where the value comes from:

<?php
// app/Services/FineCalculator.php
namespace App\Services;

use Illuminate\Container\Attributes\Config;

class FineCalculator
{
    public function __construct(
        #[Config('library.fine_cents_per_day', 25)] private int $centsPerDay,   // a config value, injected
    ) {}

    public function fineFor(int $daysLate): int
    {
        return max(0, $daysLate) * $this->centsPerDay;
    }
}

The other contextual attributes inject a specific driver or value: #[Storage('s3')] Filesystem $disk, #[Cache('redis')] Repository $cache, #[DB('mysql')] Connection $db, #[Log('daily')] LoggerInterface $log, #[Auth('web')] Guard $auth, #[Context('uuid')] string $uuid, #[RouteParameter('book')] Book $book, #[CurrentUser] User $user, #[Give(DatabaseRepository::class)] UserRepository $users and #[Tag('reports')] iterable $reports. Controllers, middleware, jobs, listeners and commands all get their constructor (and, for controllers and handle() methods, their method) parameters this way.

Binding interfaces to implementations

An interface cannot be instantiated, so the container must be told which class to use. Since Laravel 12 this can be declared on the interface with #[Bind], including a different implementation per environment:

<?php
// app/Contracts/CoverStorage.php
namespace App\Contracts;

use App\Services\FakeCoverStorage;
use App\Services\LocalCoverStorage;
use Illuminate\Container\Attributes\Bind;

#[Bind(LocalCoverStorage::class)]
#[Bind(FakeCoverStorage::class, environments: ['testing'])]
interface CoverStorage
{
    public function url(string $path): string;
}

The same binding in a service provider, which is how it is written in older code (and still the way for bindings that need logic):

<?php
// app/Providers/AppServiceProvider.php, register()
$this->app->bind(CoverStorage::class, LocalCoverStorage::class);
$this->app->bind(CoverStorage::class, fn ($app) => new S3CoverStorage($app['config']['services.covers.bucket']));

Lifetimes: bind, singleton, scoped, instance

Method (attribute) Lifetime

bind()

A new object every time it is resolved

singleton() (#[Singleton])

One object for the lifetime of the application (one request under PHP-FPM; the whole worker under Octane)

scoped() (#[Scoped])

One object per request, queued job or command, reset by the framework between them, also under Octane

instance()

Register an object that already exists

The whole demonstration below ran in php artisan tinker in the Bookshelf application:

<?php
use App\Contracts\CoverStorage;
use App\Services\BookPresenter;
use App\Services\FineCalculator;

$presenter = app(BookPresenter::class);
echo get_class(app(CoverStorage::class)), ' -> ', $presenter->cover(new App\Models\Book(['cover_path' => 'kindred.png'])), "\n";
// App\Services\LocalCoverStorage -> /storage/covers/kindred.png

app()->bind('counter.bind', fn () => new ArrayObject());
app()->singleton('counter.singleton', fn () => new ArrayObject());
app()->scoped('counter.scoped', fn () => new ArrayObject());
foreach (['bind', 'singleton', 'scoped'] as $kind) {
    printf("%-9s same instance twice: %s\n", $kind, var_export(app("counter.$kind") === app("counter.$kind"), true));
}
// bind      same instance twice: false
// singleton same instance twice: true
// scoped    same instance twice: true
app()->forgetScopedInstances();   // what Laravel does between requests and jobs (and Octane between requests)

config(['library.fine_cents_per_day' => 30]);
echo app(FineCalculator::class)->fineFor(4), " cents\n";    // 120 cents: #[Config] read the new value

Choose scoped() over singleton() for anything that holds request data (the current user, a request-specific locale, a per-request cache); under Octane a singleton would carry that data into the next user’s request. Pure, stateless services can be singletons or plain autowired classes.

Contextual binding, tagging, method injection and events

Contextual binding gives one consumer a different implementation than everyone else. Tagging groups bindings so that a consumer can receive all of them. app()->call() resolves the parameters of any callable, which is how controller methods and job handle() methods receive dependencies. Container events run code whenever a type is resolved. Continuing the same Tinker session:

<?php
// Contextual binding: BookPresenter gets the fake storage, everyone else the real one
app()->when(BookPresenter::class)->needs(CoverStorage::class)->give(App\Services\FakeCoverStorage::class);
echo app(BookPresenter::class)->cover(new App\Models\Book(['cover_path' => 'kindred.png'])), "\n";
// https://fake.test/kindred.png

// Tagging
app()->tag([App\Reports\LoansReport::class, App\Reports\ReviewsReport::class], 'reports');
echo implode(', ', array_map(fn ($r) => $r->name(), iterator_to_array(app()->tagged('reports')))), "\n";
// loans, reviews

// Method injection: call() resolves the method's parameters too
echo app()->call(fn (FineCalculator $fines, int $days) => $fines->fineFor($days), ['days' => 2]), "\n";   // 60

// Container events
app()->resolving(FineCalculator::class, fn ($calc) => print("resolving a FineCalculator\n"));
app(FineCalculator::class);                                          // resolving a FineCalculator

// PSR-11
var_dump(app() instanceof Psr\Container\ContainerInterface);         // bool(true)
var_dump(app()->has(FineCalculator::class));                         // bool(false): never bound explicitly
var_dump(app()->get('counter.singleton') instanceof ArrayObject);    // bool(true)

The PSR-11 result is a pitfall for code written against the generic interface: has() is false for a class that was never bound, even though get() (and make()) would autowire it.

Service providers

Service providers are where an application registers its bindings and boots its features. Each has two methods, called in two passes over all providers (see Request Lifecycle):

  • register(): only bind things into the container. Do not resolve services here; they may not be registered yet.

  • boot(): everything is registered; configure the application (rate limiters, gates, view composers, model observers, macros). boot() may type-hint dependencies, which the container injects.

Simple bindings can be declared as properties instead of code: public $bindings = [Interface::class => Implementation::class] and public $singletons = [...]. New applications have one AppServiceProvider; add more with php artisan make:provider, which also registers them in bootstrap/providers.php.

A deferred provider (implementing DeferrableProvider and listing its services in provides()) is loaded only when one of those services is first needed, which saves work on requests that do not use it:

<?php
// app/Providers/IsbnServiceProvider.php
namespace App\Providers;

use App\Services\IsbnLookup;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class IsbnServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(IsbnLookup::class, fn ($app) => new IsbnLookup(config('services.isbn.endpoint')));
    }

    /** The services this provider registers: it is loaded only when one of them is first resolved. */
    public function provides(): array
    {
        return [IsbnLookup::class];
    }
}
<?php
// routes/web.php
Route::get('/deferred-demo', function () {
    $loaded = fn () => array_key_exists(App\Providers\IsbnServiceProvider::class, app()->getLoadedProviders());
    $before = $loaded();
    app(App\Services\IsbnLookup::class);
    return ['before' => $before, 'after' => $loaded()];
});
// GET /deferred-demo -> {"before":false,"after":true}

The same check inside php artisan tinker reports true both times: the console kernel calls loadDeferredProviders() when it bootstraps, so deferral only matters for HTTP requests. Laravel records which providers are deferred in bootstrap/cache/services.php, rebuilt automatically when the provider list changes.

Facades

A facade is a class with static methods that forwards each call to an object in the container. Cache::get('x') is app('cache')->get('x'): the facade’s getFacadeAccessor() returns the container key, and the first call resolves and caches the object behind it (the facade root):

<?php
echo get_class(Illuminate\Support\Facades\Cache::getFacadeRoot()), "\n";   // Illuminate\Cache\CacheManager
echo get_class(Illuminate\Support\Facades\Route::getFacadeRoot()), "\n";   // Illuminate\Routing\Router
$accessor = new ReflectionMethod(Illuminate\Support\Facades\Cache::class, 'getFacadeAccessor');
echo $accessor->invoke(null), "\n";                                        // cache
var_dump(cache() === Illuminate\Support\Facades\Cache::getFacadeRoot());   // bool(true): the helper is the same object

Real-time facades make any class usable as a facade by prefixing its namespace with Facades\: Facades\App\Services\FineCalculator::fineFor(1) resolved a FineCalculator from the container and printed 30 cents via a real-time facade.

Facades, helpers or injection? All three reach the same objects, so the choice is about readability and testability:

  • Constructor injection makes a class’s dependencies explicit in its signature and is the natural choice for services and anything with many collaborators. A constructor with ten parameters is a sign the class does too much, which facades would hide.

  • Facades and helpers (Cache::, cache(), now(), route()) keep controllers, routes and views short. Both can be faked in tests, so they do not make code untestable.

Tests can swap bindings and mock facades:

<?php
// tests/Feature/ContainerTest.php
use App\Contracts\CoverStorage;
use App\Services\FakeCoverStorage;
use App\Services\FineCalculator;
use Illuminate\Support\Facades\Cache;

test('the testing environment gets the fake implementation from #[Bind]', function () {
    expect(app(CoverStorage::class))->toBeInstanceOf(FakeCoverStorage::class);
});

test('a binding can be swapped for one test', function () {
    $this->instance(FineCalculator::class, new class extends FineCalculator {
        public function __construct() {}
        public function fineFor(int $daysLate): int { return 999; }
    });

    expect(app(FineCalculator::class)->fineFor(1))->toBe(999);
});

test('facades can be mocked', function () {
    Cache::shouldReceive('get')->once()->with('books.count')->andReturn(42);

    expect(Cache::get('books.count'))->toBe(42);
});
$ php artisan test --filter=ContainerTest
   PASS  Tests\Feature\ContainerTest
  ✓ the testing environment gets the fake implementation from #[Bind]    0.09s
  ✓ a binding can be swapped for one test                                0.01s
  ✓ facades can be mocked                                                0.01s
  Tests:    3 passed (4 assertions)

The first test shows the environment-specific #[Bind] at work: under APP_ENV=testing the container picked FakeCoverStorage without any test setup.

Contracts

Contracts are the interfaces of Laravel’s services (Illuminate\Contracts\Cache\Repository, Illuminate\Contracts\Mail\Mailer, Illuminate\Contracts\Queue\Queue, …), in the separate illuminate/contracts package. Type-hinting a contract instead of a facade or a concrete class makes a dependency explicit and swappable, and is the usual choice in packages meant to work outside Laravel. In application code the difference from a facade is mostly style: the container resolves the same object either way.

<?php
namespace App\Services;

use Illuminate\Contracts\Cache\Repository as Cache;

class CatalogueStats
{
    public function __construct(private Cache $cache) {}

    public function bookCount(): int
    {
        return $this->cache->remember('books.count', 3600, fn () => \App\Models\Book::count());
    }
}

Where older books and tutorials are outdated

  • Binding interfaces only in service providers (Stauffer ch. 11) still works, but #[Bind], #[Singleton] and #[Scoped] on the interface or class (Laravel 12) remove most provider code.

  • $this->app->make() and resolve() everywhere: type-hint the dependency in a constructor or method instead, or use a contextual attribute (#[Config], #[Storage], #[CurrentUser]) rather than resolving inside the method.

  • Five default providers (Auth, Broadcast, Event, Route, App) in Laravel 5.8 are one AppServiceProvider since Laravel 11.

  • singleton() for request-specific services was harmless under PHP-FPM; with Octane it leaks data between requests, so use scoped().