4 / 12
Guide Book Β· Chapter 4

Facades, helpers & contracts

What `Route::get()` really does, why facades are not static calls, and how to choose between a facade, a helper and an injected contract.

Cache::get('key') looks like a static method call. It is not one β€” and the difference matters for testing, for tooling and for understanding error messages.

A facade is a proxy to a container binding

Every facade extends Facade and answers one question: which container key do I stand for?

php
<?php

use Illuminate\Support\Facades\Facade;

class Greeter
{
    public function hello(string $name): string
    {
        return "Hello, $name";
    }
}

class Greet extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'greeter';       // a container key
    }
}

app()->singleton('greeter', fn () => new Greeter());

echo Greet::hello('Ada'), PHP_EOL;          // looks static…
echo app('greeter')->hello('Ada'), PHP_EOL; // …is exactly this

__callStatic intercepts the call, resolves greeter from the container, and forwards the call to the real object.

php
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Config;

echo get_class(Route::getFacadeRoot()), PHP_EOL;    // Illuminate\Routing\Router
echo get_class(Config::getFacadeRoot()), PHP_EOL;   // the config repository
var_dump(Route::getFacadeRoot() === app('router'));

Why this matters: facades are swappable

Because the call goes through the container, a test can replace the underlying object. Facade::swap() and the shouldReceive/fake helpers all work this way.

php
<?php

use Illuminate\Support\Facades\Facade;

class Clock
{
    public function now(): string { return 'the real time'; }
}

class FrozenClock extends Clock
{
    public function now(): string { return '2026-01-15 09:00'; }
}

class Time extends Facade
{
    protected static function getFacadeAccessor(): string { return 'clock'; }
}

app()->singleton('clock', fn () => new Clock());
echo Time::now(), PHP_EOL;

Time::swap(new FrozenClock());      // the whole point of the indirection
echo Time::now(), PHP_EOL;

A genuinely static method could not do that. This is the answer to "aren't facades just global state?" β€” they are global access, but not global binding.

The framework's own fakes

Most first-party facades ship a fake that records instead of acting.

php
<?php

use Illuminate\Support\Facades\Event;

class OrderShipped
{
    public function __construct(public string $order) {}
}

Event::fake();

Event::dispatch(new OrderShipped('A-1'));
Event::dispatch(new OrderShipped('A-2'));

echo 'recorded dispatches: ', count(Event::dispatched(OrderShipped::class)), PHP_EOL;
var_dump(Event::hasDispatched(OrderShipped::class));
echo 'nothing actually ran β€” the fake only recorded', PHP_EOL;

In a test you would assert on that recording with Event::assertDispatched(OrderShipped::class, 2). Mail::fake(), Queue::fake(), Storage::fake(), Http::fake() and Notification::fake() all follow the same pattern.

Helpers

Helper functions are the shortest form. Many are one-line wrappers over the same bindings.

php
<?php

echo config('app.name', 'Laravel'), PHP_EOL;
echo str('hello world')->title(), PHP_EOL;
echo collect([3, 1, 2])->sort()->implode(','), PHP_EOL;
echo now()->format('Y'), PHP_EOL;

print_r(data_get(['user' => ['roles' => ['admin']]], 'user.roles.0'));
echo PHP_EOL;
echo value(fn () => 'lazy'), PHP_EOL;
echo blank('') ? 'blank' : 'filled', PHP_EOL;
echo filled('x') ? 'filled' : 'blank', PHP_EOL;

Contracts: the third option

A contract is the interface behind a facade. Type-hint it and the container injects the implementation β€” the same object the facade would have reached.

php
<?php

use Illuminate\Contracts\Config\Repository as ConfigContract;

class PricingService
{
    public function __construct(private ConfigContract $config) {}

    public function currency(): string
    {
        return $this->config->get('app.currency', 'GBP');
    }
}

echo app(PricingService::class)->currency(), PHP_EOL;

Choosing between the three

FacadeHelperInjected contract
Reads asCache::get()cache()->get()$this->cache->get()
Dependencieshiddenhiddenvisible in the signature
Unit-testable without the frameworknonoyes
Best inroutes, providers, quick scriptsviews, closuresservices and domain classes
php
<?php

use Illuminate\Contracts\Cache\Repository as CacheContract;

// A service class: dependencies declared, trivially testable with a stub.
class ReportService
{
    public function __construct(private CacheContract $cache) {}

    public function summary(): string
    {
        return $this->cache->remember('summary', 60, fn () => 'computed once');
    }
}

echo app(ReportService::class)->summary(), PHP_EOL;
echo app(ReportService::class)->summary(), PHP_EOL;   // served from cache

πŸ’‘ Tip: A useful rule: facades and helpers in the framework's own seams β€” routes, providers, Blade, commands β€” and constructor injection in the classes that hold your business logic. A class whose constructor lists its four dependencies is a class whose test needs no application at all.

Real-time facades

Prefix any class with Facades\ and Laravel builds a facade for it on the fly.

php
<?php

class Publisher
{
    public function publish(string $slug): string
    {
        return "published $slug";
    }
}

// Normally: use Facades\Publisher; then Publisher::publish('x')
// The mechanism is the same β€” the container resolves the real class.
echo app(Publisher::class)->publish('hello-world'), PHP_EOL;

They are handy for making an existing class mockable without changing its callers. They also hide dependencies just as thoroughly as any other facade, so the same guidance applies.

Reference: Facades and Contracts.

lesson.php
Loading editor…
No output yet
Press Run or βŒ˜β†΅ to compile and run your code.

Search

Jump to a lesson or a past run