skip to content

Facades vs Injection

A Laravel facade forwards static calls to a container binding through getFacadeAccessor and __callStatic. Interviewers ask whether facades hide dependencies and how they compare with injection.

on this pageshow

explore

questions

5

In Laravel, what is the difference between using the Cache facade, the cache() helper and an injected Illuminate\Contracts\Cache\Repository?

level: juniorimportance: must knowfreq 66%

answer

  1. same container, three doorways
  2. helper calls app('cache')
  3. cache.store alias for the contract
  4. constructor makes the dependency visible
  5. Factory contract when you need store()

basics

~20 s

All three reach the same container-managed cache service, so behaviour matches. The facade and cache() helper call it from inside method bodies; an injected Repository contract declares the dependency in the constructor, which is explicit and framework-portable.

solid answer

~40 s

They are three routes to the same service container. `Cache::get('k')` resolves the `'cache'` binding, a `CacheManager`, and forwards to the default store. The `cache('k')` helper is a function that runs `app('cache')->get('k')`, so it is the same object without an import. Type-hinting `Illuminate\Contracts\Cache\Repository` in a constructor lets auto-wiring inject the `'cache.store'` binding, the default store's `Repository`. Data and behaviour are identical; the difference is where the dependency is declared. With the facade or helper it is hidden in a method body; with the contract it is in the constructor, so the class's needs are visible and it depends only on an `illuminate/contracts` interface. If you need to pick stores with `store('redis')`, inject `Illuminate\Contracts\Cache\Factory` instead of `Repository`.

code

php · 26 lines
php
<?php

namespace App\Crm;

use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

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

    public function viaContract(int $id): mixed
    {
        return $this->cache->get("account:{$id}");
    }

    public function viaFacade(int $id): mixed
    {
        return Cache::get("account:{$id}");
    }

    public function viaHelper(int $id): mixed
    {
        return cache("account:{$id}");
    }
}

go deeper

for a junior

Know the three styles by sight and that they all reach the same cache service through the container.

for a middle

Explain which binding each resolves: cache for the facade and helper, cache.store for the Repository contract, and when Factory is the right type-hint.

for a senior

Argue when explicit constructor dependencies pay off, such as domain services and packages, and when facade brevity in controllers is acceptable.

for a principal

Set a consistent convention per layer so the codebase reads uniformly, rather than letting each developer pick a style file by file.

## One service, three ways in Laravel registers its cache system in the **service container** (the object that builds and shares services). `CacheServiceProvider` binds two singletons: `'cache'`, an `Illuminate\Cache\CacheManager` that manages all configured stores, and `'cache.store'`, the `Illuminate\Cache\Repository` for the default store named in `config('cache.default')`. The skeleton's `.env.example` sets `CACHE_STORE=database`. The container also registers **aliases**, so asking for `Illuminate\Contracts\Cache\Factory` returns the `'cache'` manager and asking for `Illuminate\Contracts\Cache\Repository` returns the `'cache.store'` repository. A **contract** in Laravel is simply an interface from the `Illuminate\Contracts` namespace that describes a core service. | Style | Example | Resolves | Dependency is declared | |---|---|---|---| | Facade | `Cache::get('k')` | `'cache'` (`CacheManager`, forwarding to default store) | nowhere; only a `use` import | | Helper | `cache('k')` | `app('cache')`, then `get('k')` | nowhere; no import needed | | Injected contract | `__construct(private Repository $cache)` | `'cache.store'` (`Repository`) | in the constructor signature | ## How each one works - **Facade.** `Illuminate\Support\Facades\Cache` has no `get` method. Its inherited `__callStatic()` resolves the `'cache'` binding and calls `get()` on it; `CacheManager` forwards that to the default store. - **Helper.** `cache()` is a global function in `Illuminate/Foundation/helpers.php`. With no arguments it returns `app('cache')`; with a string it returns `app('cache')->get($key, $default)`; with an array it calls `put()` using the second argument as the TTL. Many helpers mirror a facade this way: `view()`, `config()`, `response()`. - **Injected contract.** When the container builds a controller, job, listener or service, it reads constructor type-hints and resolves each one. `Repository $cache` receives the default store's repository. ## What differs, and what does not What does **not** differ: 1. The underlying object and data. All three talk to the same singletons, so a value written with the facade is readable through the helper and the injected repository. 2. Testability in feature tests. Laravel's documentation states there is "absolutely no practical difference between facades and helper functions", and the facade's test tooling works for helper calls too because both resolve the same container binding. What **does** differ: - **Visibility of dependencies.** A constructor that lists `Repository $cache` tells a reader and a static-analysis tool exactly what the class needs. Facade and helper calls are discovered only by reading the method bodies. - **Coupling.** An injected contract ties the class to an interface in `illuminate/contracts`, which a package can depend on without the full framework. A facade ties the class to `Illuminate\Support\Facades` and to a booted Laravel application. - **Choosing a store.** The injected `Repository` is already the default store. To call `store('redis')` you need the manager, so inject `Illuminate\Contracts\Cache\Factory` (the facade's equivalent contract) instead. - **Imports and typing.** The helper needs no import at all; the facade needs one `use` line; the contract gives a real typed property your editor understands without facade docblocks. ## A common slip Type-hinting the facade class itself — `__construct(private Cache $cache)` with `Illuminate\Support\Facades\Cache` — does not inject the cache. The container builds an instance of the facade class, and calling `$this->cache->get()` fails with an undefined-method error, because facades only forward **static** calls. Inject the contract, or keep using the facade statically. A second slip is type-hinting the concrete class when a contract exists. Asking for `Illuminate\Cache\Repository` works, because the container aliases it to `'cache.store'` too, but it ties the class to the framework's implementation rather than to the interface. The contract is the more stable choice, and it is what the documentation's contract reference lists beside each facade. ## Other services follow the same pattern The cache is only one example. Most framework services have all three doorways: | Service | Facade | Helper | Contract to inject | |---|---|---|---| | Configuration | `Config::get('app.name')` | `config('app.name')` | `Illuminate\Contracts\Config\Repository` | | Views | `View::make('profile')` | `view('profile')` | `Illuminate\Contracts\View\Factory` | | Events | `Event::dispatch($e)` | `event($e)` | `Illuminate\Contracts\Events\Dispatcher` | Not every facade has a helper, and not every contract has a facade, but where both exist they normally resolve the same container binding. ## Choosing in practice Controllers, route closures and small glue code often use facades or helpers for brevity. Services with real logic, and anything shipped as a package, tend to inject contracts so their collaborators are explicit. Laravel's documentation treats this as a team preference: both styles produce working, testable applications as long as classes stay focused.

  • When would you type-hint Illuminate\Contracts\Cache\Factory rather than Repository?
    When the class must choose between stores at runtime, for example `->store('redis')` for hot data and the default store for the rest. `Factory` resolves to the `CacheManager`, which exposes `store()`; `Repository` is already bound to one store, the default, so it cannot switch.
  • Does the cache() helper create a new cache object on every call?
    No. It calls `app('cache')`, and `'cache'` is registered as a singleton, so the container returns the same `CacheManager` each time within the application instance. The helper is a shortcut over the container, not a separate cache implementation.

saying these in an interview costs you the question

  • Helpers bypass the container and build a fresh service each call
  • Facade calls cannot be covered by tests, only injected contracts can
  • Injecting the Cache facade class gives you the cache repository
  • The facade and the contract talk to different cache stores
  • Facades are faster because static calls skip container resolution
open as a page

In Laravel, how does a call like Cache::get('key') reach a real object when the Cache facade class defines no static get method?

level: middleimportance: must knowfreq 72%

basics

~10 s

Cache extends Illuminate\Support\Facades\Facade, whose __callStatic() catches the missing static call, resolves the container binding named by getFacadeAccessor() ('cache', a CacheManager) and calls get() on that object.

open as a page

In a Laravel CRM code review, a service calls the Cache, Http and Log facades inside its methods — do facades hide dependencies, and what would you change?

level: seniorimportance: should knowfreq 55%

basics

~10 s

Yes: facade calls keep collaborators out of the constructor, so the class looks dependency-free and its facade calls fail outside a booted app. Inject Illuminate\Contracts\Cache\Repository, Illuminate\Http\Client\Factory and Psr\Log\LoggerInterface to make them explicit.

open as a page

A custom Laravel facade proxies a service registered with bind(), yet every facade call gets the same object — why, and when does that reuse reset?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The Facade base class stores the first resolved object in a static $resolvedInstance map while static::$cached is true, so bind()'s fresh-instance rule applies only once. Bootstrap, the queue worker's per-job reset and clearResolvedInstance() empty that map.

open as a page

In Laravel, how does a real-time facade imported as use Facades\App\Crm\LeadScorer; work, and what does it cost you?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Prefixing an import with Facades\ makes Laravel's AliasLoader generate a facade class whose getFacadeAccessor() returns the rest of the name, so static calls resolve that class from the container. It saves injection but hides the dependency.

open as a page