Facades Explained - Magic or Practical?
Understand how Laravel Facades work under the hood and when to use them
How Facades Work:
1. You call Cache::get('key')
2. PHP triggers __callStatic() magic method
3. Facade resolves the real service from the container
4. The method is called on the resolved instance
Arguments FOR Facades:
✅ Clean, memorable syntax (Cache::get() vs $this->cache->get())
✅ Fully testable with facade mocks
✅ No need to inject common services in every constructor
✅ IDE-friendly with proper docblocks
Arguments AGAINST Facades:
❌ Adds "magic" that can be harder to trace
❌ Can lead to hidden dependencies
❌ Makes class dependencies less obvious
❌ Can encourage poor design if overused
Real-World Decision:
Use Facades for framework-provided services (Cache, DB, Mail) in controllers and simple use cases. Use Dependency Injection for your own business logic services and when building reusable packages.
Code Examples
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmation;
class OrderController extends Controller
{
public function store(Request $request)
{
// Start database transaction
DB::beginTransaction();
try {
// Create order
$order = Order::create($request->all());
// Cache the order for quick lookup
Cache::put("order.{$order->id}", $order, now()->addHour());
// Send confirmation email
Mail::to($order->customer_email)
->send(new OrderConfirmation($order));
DB::commit();
return response()->json($order);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}
// Benefits:
// ✅ Very readable and concise
// ✅ No need to inject Cache, DB, Mail in constructor
// ✅ Perfect for controllers and quick scripts
💡 Facades provide a clean, static-like syntax. Cache::get() is more readable than $this->cache->get(). Great for framework services in controllers.
<?php
namespace App\Http\Controllers;
use Illuminate\Cache\Repository as CacheRepository;
use Illuminate\Database\Connection;
use Illuminate\Mail\Mailer;
use App\Mail\OrderConfirmation;
class OrderController extends Controller
{
public function __construct(
protected CacheRepository $cache,
protected Connection $db,
protected Mailer $mail
) {}
public function store(Request $request)
{
// Start database transaction
$this->db->beginTransaction();
try {
// Create order
$order = Order::create($request->all());
// Cache the order
$this->cache->put("order.{$order->id}", $order, now()->addHour());
// Send confirmation email
$this->mail->to($order->customer_email)
->send(new OrderConfirmation($order));
$this->db->commit();
return response()->json($order);
} catch (\Exception $e) {
$this->db->rollBack();
throw $e;
}
}
}
// Benefits:
// ✅ Explicit dependencies visible in constructor
// ✅ Better for packages and reusable components
// ✅ IDE autocomplete works perfectly
// Drawbacks:
// ❌ More verbose
// ❌ Constructor can get bloated with many dependencies
💡 Dependency Injection makes dependencies explicit. Better for complex services and packages, but more verbose for simple controllers.
<?php
// When you call:
Cache::get('user.123');
// Here's what actually happens:
// Step 1: Cache facade is called
class Cache extends Facade
{
// Step 2: This method tells Laravel which service to resolve
protected static function getFacadeAccessor()
{
return 'cache'; // Resolve 'cache' from container
}
}
// Step 3: Facade base class magic
abstract class Facade
{
public static function __callStatic($method, $args)
{
// Resolve the actual service from container
$instance = static::getFacadeRoot();
// Call the method on the real object
return $instance->$method(...$args);
}
public static function getFacadeRoot()
{
// Get the service from the container
return static::resolveFacadeInstance(
static::getFacadeAccessor()
);
}
}
// Step 4: The real cache repository is resolved
$cacheRepository = app('cache'); // From container
$cacheRepository->get('user.123'); // Actual method call
// So Cache::get() is just a shortcut for:
// app('cache')->get()
// It's NOT a true static method!
💡 Facades use PHP's __callStatic() magic method to forward calls to real service instances from the container. They're not static classes, they're dynamic proxies.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmation;
class OrderTest extends TestCase
{
public function test_order_creation_sends_email()
{
// Facade mocking is super easy!
Mail::fake();
Cache::shouldReceive('put')
->once()
->with('order.1', \Mockery::any(), \Mockery::any());
// Make request
$response = $this->postJson('/orders', [
'customer_email' => 'test@example.com',
'total' => 100,
]);
// Assert email was sent
Mail::assertSent(OrderConfirmation::class);
$response->assertStatus(201);
}
public function test_cache_is_used()
{
Cache::shouldReceive('get')
->once()
->with('user.123')
->andReturn(['name' => 'John']);
$user = Cache::get('user.123');
$this->assertEquals('John', $user['name']);
}
}
// Facade testing is:
// ✅ Simple and readable
// ✅ Built-in fake() methods for common facades
// ✅ Mockery integration for custom assertions
💡 Facades are fully testable! Use fake() for Mail, Storage, Queue, etc., or shouldReceive() for custom mocking. Testing is actually easier than with dependency injection.
<?php
// Step 1: Create your service class
namespace App\Services;
class ReportGenerator
{
public function generateSalesReport(string $period): array
{
return [
'period' => $period,
'total_sales' => 15000,
'total_orders' => 120,
];
}
}
// Step 2: Create the Facade
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class Report extends Facade
{
protected static function getFacadeAccessor()
{
return 'report.generator'; // Container binding key
}
}
// Step 3: Register in AppServiceProvider
namespace App\Providers;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton('report.generator', function ($app) {
return new \App\Services\ReportGenerator();
});
}
}
// Step 4: Use your custom facade!
namespace App\Http\Controllers;
use App\Facades\Report;
class DashboardController extends Controller
{
public function index()
{
// Clean, static-like syntax
$sales = Report::generateSalesReport('monthly');
return view('dashboard', compact('sales'));
}
}
// Now you have: Report::generateSalesReport()
// Instead of: app('report.generator')->generateSalesReport()
💡 You can create custom facades for your own services. Great for frequently-used services that you want accessible everywhere with clean syntax.