Facades are everywhere in Laravel code; knowing they're just container lookups makes testing and debugging straightforward.
A facade like Cache::get('key') looks static but isn't. Cache extends Illuminate\Support\Facades\Facade, and its only job is getFacadeAccessor(), returning a container key such as 'cache'. When you call a static method, PHP's __callStatic magic resolves that key from the container (caching the instance) and forwards the call. So Cache::get() is equivalent to app('cache')->get().
Consequences:
- Facades are as testable as injected services.
Cache::shouldReceive('get')->andReturn(...)swaps in a Mockery mock;Mail::fake(),Queue::fake()andStorage::fake()swap in purpose-built fakes.Facade::swap($instance)replaces the root. - The underlying class is shown in the facade class reference in the docs and in each facade's
@seeand@methoddocblocks, which IDE helpers read. - Real-time facades: prefix any class's namespace with
Facades\(use Facades\App\Services\Geo;) and Laravel generates a facade for it on the fly.
Helpers such as cache(), config() and app() are the same container lookups in function form. Choose one style per codebase; constructor injection makes dependencies most visible.
Many core classes are Macroable: Collection::macro('toCsv', fn () => ...), Str::macro, Request::macro, registered in a provider's boot(). Other extension points include custom Eloquent casts, custom cache, queue or filesystem drivers via extend(), and the Manager pattern behind them.
Reading framework source is the best way to answer "how does this work?": everything lives in vendor/laravel/framework/src/Illuminate.
Example
<?php
// What a facade boils down to
class Cache extends \Illuminate\Support\Facades\Facade
{
protected static function getFacadeAccessor(): string { return 'cache'; }
}
// Cache::get('k') == app('cache')->get('k')
// Testing with facades
Cache::shouldReceive('get')->once()->with('rate:eur')->andReturn(1.08);
$this->assertSame(1.08, app(FxService::class)->rate('eur'));
// Real-time facade for your own class
use Facades\App\Services\Geo; // generated on the fly
Geo::shouldReceive('country')->andReturn('AM');
echo Geo::country('1.2.3.4'); // AM
// Macro: extend a core class (in AppServiceProvider::boot)
use Illuminate\Support\Collection;
Collection::macro('toCsv', function (): string {
return $this->map(fn ($row) => implode(',', $row))->implode("\n");
});
echo collect([[1, 'a'], [2, 'b']])->toCsv(); // "1,a\n2,b"Remember
- A facade forwards static calls to a service resolved from the container
- Facades can be mocked with shouldReceive or replaced with fakes
- Real-time facades and macros extend the framework without forking it
Watch out for
- Believing facades are untestable static calls and wrapping every one of them in custom interfaces.