Facades vs Dependency Injection

Two ways to reach a service — when the convenient one is fine, and when to prefer the explicit one.

Laravel gives you two idioms for using a service, and juniors often think they must pick a side. The truth is more nuanced: they resolve to the same container instance — the choice is about readability and testability, not capability.

Facades

Cache::get('key') looks static but is not. Under the hood the facade asks the container for the real cache service and forwards the call. Convenient, terse, great in Blade and quick scripts.

Constructor injection

Type-hint the dependency and let the container pass it in. The class now declares what it needs — its dependencies are honest and visible in the signature.

// Facade
use Illuminate\Support\Facades\Cache;
Cache::put('k', 'v', 60);

// Injection
public function __construct(private CacheRepository $cache) {}
$this->cache->put('k', 'v', 60);

Testing both

Facades are still testable — Cache::fake(), Queue::fake(), Event::fake() swap in test doubles with one line. Injected dependencies you mock the ordinary way.

Example

Example · php
<?php

// --- Inner layer: explicit injection, honest dependencies ---
namespace App\Actions;

use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Events\Dispatcher;

class PublishArticle
{
    public function __construct(
        private Cache $cache,
        private Dispatcher $events,
    ) {}

    public function handle(\App\Models\Article $article): void
    {
        $article->update(['published_at' => now()]);
        $this->cache->forget("article:{$article->id}");
        $this->events->dispatch(new \App\Events\ArticlePublished($article));
    }
}

// --- Outer layer: facades for brevity, faked in tests ---
namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class HomeController
{
    public function __invoke()
    {
        return Cache::remember('home:stats', 300, fn () => [
            'users' => \App\Models\User::count(),
        ]);
    }
}

// --- In a test ---
// Cache::fake();  Event::fake();
// (new PublishArticle(app(Cache::class), app(Dispatcher::class)))->handle($article);
// Event::assertDispatched(ArticlePublished::class);

When to use it

  • A developer refactors a controller that uses Cache:: and Mail:: facades into one that receives CacheRepository and Mailer via constructor injection, making the class fully testable without faking facades.
  • A package author uses dependency injection instead of facades so the package works even in environments where the facade aliases are not registered.
  • A senior engineer audits a codebase and replaces static facade calls in service classes with injected interfaces, making the dependency graph explicit and visible in the constructor.

More examples

Facade vs constructor injection

Both approaches call the same underlying service; the DI version makes the cache dependency explicit and easily swappable in tests.

Example · php
// Facade approach
use Illuminate\Support\Facades\Cache;

class ProductService
{
    public function featured(): Collection
    {
        return Cache::remember('featured', 3600, fn() => Product::featured()->get());
    }
}

// Dependency injection approach (preferred for testability)
use Illuminate\Contracts\Cache\Repository;

class ProductService
{
    public function __construct(private Repository $cache) {}

    public function featured(): Collection
    {
        return $this->cache->remember('featured', 3600, fn() => Product::featured()->get());
    }
}

Swap a facade in a test

Facades offer ::fake() helpers; injected interfaces are tested by swapping the container binding with a mock, giving more precise assertion control.

Example · php
// With facades, use ::fake()
Cache::fake();
$this->getJson('/products/featured');
Cache::assertPut('featured', ...);

// With injected interface, swap the binding
$mock = Mockery::mock(Repository::class);
$mock->shouldReceive('remember')->once()->andReturn(collect([]));
$this->app->instance(Repository::class, $mock);
$this->getJson('/products/featured');

When to choose each approach

Facades are idiomatic for controllers and commands; DI is preferred for service-layer classes where the dependency graph should be explicit.

Example · php
// Facades are fine in:
// - Route closures
// - Blade directives
// - Quick Artisan commands
// - Controllers (facades are testable via ::fake())

Cache::put('key', 'value', 60); // perfectly acceptable

// Prefer DI in:
// - Service / repository classes
// - Packages distributed to others
// - Classes requiring strict unit tests

class PaymentService {
    public function __construct(
        private LoggerInterface $log,
        private Mailer $mail
    ) {}
}

Discussion

  • Be the first to comment on this lesson.