Laravel's facades — Cache::get(), Route::get(), Auth::user() — look like static method calls, but they aren't. Each one is a thin proxy class that forwards the call to a real object pulled out of the service container at runtime, which is what lets facades stay fully testable and swappable despite the static-looking syntax.
1. The underlying class
Start with a normal class containing the actual logic — nothing facade-specific about it yet:
namespace App\Services;
class Greeter
{
public function greet(string $name): string
{
return "Hello, {$name}!";
}
}
2. Binding it into the service container
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton('greeter', function () {
return new \App\Services\Greeter();
});
}
singleton() means the container builds the Greeter instance once and returns that same instance on every subsequent resolution — appropriate for a stateless service like this one. Use bind() instead if a fresh instance should be created on every resolution.
3. The facade class
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class Greeter extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'greeter';
}
}
getFacadeAccessor() is the one method every facade must implement — it returns the string key the container binding was registered under (matching the 'greeter' from step 2). This string is the only link between the facade class and the actual bound instance; everything else (the static-looking __callStatic magic) lives in the base Facade class and works identically for any facade.
4. Using it
use App\Facades\Greeter;
Greeter::greet('World'); // "Hello, World!"
Calling Greeter::greet() resolves the 'greeter' binding from the container, then calls ->greet() on that real object — functionally identical to injecting Greeter (the service class) through a constructor and calling the method directly, just with different call-site syntax.
5. Why not skip the facade and just inject the service?
Constructor injection is generally the better default — it makes a class's dependencies explicit and is friendlier to testing without any extra setup. A facade earns its place mainly for something called from many unrelated places throughout an app (logging, caching, config access) where threading a constructor dependency through every one of those call sites would be far more disruptive than the facade's static-looking convenience.
6. Testing code that uses a facade
Greeter::shouldReceive('greet')
->with('World')
->andReturn('Hello, World!');
Every Laravel facade supports this Mockery-based swap automatically, inherited from the base Facade class — during a test, shouldReceive() replaces the real bound instance with a mock for the duration of that test, without changing a single line of the code under test.