0

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:

  1. Cache is a class alias in config/app.php pointing to Illuminate\Support\Facades\Cache.
  2. PHP calls the static method get() on the Facade class.
  3. Facade has a __callStatic() magic method. It catches the call.
  4. __callStatic() calls static::getFacadeRoot() which resolves the underlying instance from the container.
  5. 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-helper generates 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