Explain Facades — are they static calls or not?
Advanced5 min read·eng-10-007
interview
Concept
Laravel Facades are NOT static calls — they are a proxy pattern that resolves an instance from the service container and delegates method calls to it.
What looks like a static call:
php
Cache::get('key');What actually happens:
Cacheis a class alias inconfig/app.phppointing toIlluminate\Support\Facades\Cache.- PHP calls the static method
get()on the Facade class. Facadehas a__callStatic()magic method. It catches the call.__callStatic()callsstatic::getFacadeRoot()which resolves the underlying instance from the container.- The real method
get()is called on that instance (e.g.,Illuminate\Cache\CacheManager).
The Facade base class:
php
abstract class Facade
{
protected static function getFacadeAccessor(): string { /* 'cache', 'auth', etc. */ }
public static function __callStatic(string $method, array $args): mixed
{
$instance = static::getFacadeRoot(); // resolve from container
return $instance->$method(...$args); // call on real instance
}
}Benefits of Facades:
- Convenient, readable syntax.
- Testable via
Cache::fake(),Mail::fake(), etc. — swaps the container binding. - IDE support via
barryvdh/laravel-ide-helpergenerates proper docblocks.
Drawbacks:
- Looks like static calls — developers may not realize it's container-resolved.
- Can be used anywhere, tempting to skip dependency injection.
- Less explicit than injected dependencies.
Real-time Facades: use Facades\App\Services\OrderService; — prefixing your own class with Facades\ creates a real-time facade. PHP resolves it from the container on demand.
Testing: Cache::shouldReceive('get')->with('key')->once()->andReturn('value') — Facades delegate to Mockery under the hood via Facade::spy() / Facade::mock().
Code Example
php
<?php
// What you write:
Cache::put('user:42', $user, 3600);
$cached = Cache::get('user:42');
// What actually happens (simplified):
$facade = app('cache'); // resolve from container
$result = $facade->put('user:42', $user, 3600); // call on real instance
// Facade class (simplified):
namespace Illuminate\Support\Facades;
class Cache extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'cache'; // the container key
}
// __callStatic is inherited from Facade — no static methods needed
}
// ALL Facade calls go through __callStatic:
class Facade
{
protected static array $resolvedInstances = [];
public static function __callStatic(string $method, array $args): mixed
{
$instance = static::getFacadeRoot();
return $instance->$method(...$args);
}
protected static function getFacadeRoot(): mixed
{
$name = static::getFacadeAccessor();
// Cache the resolved instance (singleton behavior)
return static::$resolvedInstances[$name] ??= static::$app->make($name);
}
}
// Testing with Facades — swap the underlying implementation
Cache::fake(); // replaces the real cache with an in-memory fake
Cache::put('key', 'val'); // goes to the fake
Cache::assertHas('key'); // assertion on the fake
Mail::fake();
// ... code that sends mail ...
Mail::assertSent(OrderConfirmation::class, fn($mail) => $mail->hasTo('alice@example.com'));
// Dependency injection equivalent (explicitly testable, preferred in larger apps)
class ProductController extends Controller
{
public function __construct(
private readonly \Illuminate\Contracts\Cache\Repository $cache, // injected, not Facade
) {}
public function index(): JsonResponse
{
$products = $this->cache->remember('products', 3600, fn() => Product::all());
return response()->json($products);
}
}
// In tests: inject a mock or use a real in-memory cache driver