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?
answer
- the facade remembers, not the container
- static $resolvedInstance keyed by accessor
- protected static $cached defaults to true
- Schema and Pipeline opt out
- clearResolvedInstances at bootstrap and per job
basics
~20 sThe 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.
solid answer
~40 s`bind()` tells the container to build a new object on every `make()`, but a facade only calls the container once. `resolveFacadeInstance()` stores the result in the static `$resolvedInstance` array, keyed by the accessor, whenever `protected static $cached` is `true` — the default — and returns that stored object on every later call. So a facade over a `bind()` service behaves like a per-process singleton until the map is cleared. Laravel clears it in the `RegisterFacades` bootstrapper, in the test case's set-up, and in `queue:work`'s reset before each job, which also calls `forgetScopedInstances()`; the HTTP kernel clears the `Request` facade for each request. You can drop one entry with `Facade::clearResolvedInstance()` or opt a facade out with `protected static $cached = false`, as the `Schema` and `Pipeline` facades do — then each static call resolves anew.
code
php · 21 lines<?php
namespace App\Support\Facades;
use App\Crm\LeadExportBuilder;
use Illuminate\Support\Facades\Facade;
class LeadExport extends Facade
{
// Resolve through the container on every static call
// instead of reusing the first instance.
protected static $cached = false;
protected static function getFacadeAccessor(): string
{
return LeadExportBuilder::class;
}
}
// With $cached = false, keep state on one instance by chaining:
// LeadExport::addColumn('email')->addColumn('owner')->build();go deeper
Remember that a facade may hand back the same object on every call, whatever lifetime the binding was registered with.
Explain resolveFacadeInstance, the $resolvedInstance map and the $cached flag, and name the points where Laravel clears the map.
Diagnose state leaking between two uses of one facade in a request or job, and pick the fix: stateless service, singleton, injection or $cached false.
Treat facades as unsuitable for per-use stateful objects in team guidelines, so lifetime bugs are designed out rather than debugged.
## Two caches that look like one In Laravel a **binding lifetime** is the container's rule for how often it builds an object: `bind()` builds a new one on every `make()`, `singleton()` builds once per application, and `scoped()` builds once per request or job. A **facade** is a class extending `Illuminate\Support\Facades\Facade` that forwards static calls to an object resolved from the container. The surprise is that the facade keeps its **own** memory of what it resolved, independent of the container's lifetime rules. In `Facade::resolveFacadeInstance($name)`: 1. If `static::$resolvedInstance[$name]` is set, return it. 2. Otherwise, if `static::$cached` is `true`, resolve `static::$app[$name]` **and store it** in `$resolvedInstance`. 3. If `$cached` is `false`, resolve `static::$app[$name]` and return it without storing. `protected static $cached = true;` is the base-class default. So a facade over a service registered with `bind()` asks the container once, keeps the object, and every later static call in the same process reuses it. The container's "new each time" rule is simply never consulted again. ## When the map is emptied | Where | What it calls | Effect | |---|---|---| | `RegisterFacades` bootstrapper | `Facade::clearResolvedInstances()` then `setFacadeApplication($app)` | a freshly bootstrapped app starts with an empty map | | HTTP kernel, `sendRequestThroughRouter()` | `Request::clearResolvedInstance()` | the `Request` facade always sees the current request | | `queue:work` loop, before each job | `forgetScopedInstances()` and `Facade::clearResolvedInstances()` | each job starts with fresh facade roots | | Test lifecycle set-up | `Facade::clearResolvedInstances()` | tests do not inherit another test's roots | | Your code | `SomeFacade::clearResolvedInstance()` | drops one accessor's stored object | Under PHP-FPM every request is a new PHP execution, so static properties die with the request anyway; the map matters inside one request, inside a long-running `queue:work` process between the resets above, and in any process that keeps the application alive between units of work. ## Why it bites - **Stateful services.** Suppose a CRM `LeadExportBuilder` collects columns in a property and is bound with `bind()` so every export starts clean. Accessed through a facade, the second export in the same request inherits the first export's columns, because it is the same object. - **Late rebinding.** If code calls `app()->instance(LeadExportBuilder::class, $other)` after the facade has already resolved, the container changes but the facade still returns its stored object. `forgetInstance()` on the container has the same blind spot. Call the facade's `clearResolvedInstance()` as well. - **Surprising identity.** Code that compares `LeadExport::getFacadeRoot()` with a freshly made `app(LeadExportBuilder::class)` finds two different objects, which confuses anyone who assumes the facade is a thin alias for `make()`. ## The opt-out and its own trap Setting `protected static $cached = false;` makes every static call resolve through the container again, which restores `bind()` semantics. Laravel's own `Schema` and `Pipeline` facades do this, because each call should get a builder or pipeline suited to that call. It changes behaviour in another way too: with a `bind()` service, `LeadExport::addColumn('email'); LeadExport::build();` now runs on **two different** objects, so the column is lost. State then has to travel by chaining on the returned instance (`LeadExport::addColumn('email')->build()`), or you should inject the builder, or resolve it once with `app(LeadExportBuilder::class)`. ## Reproducing it in a few lines A quick way to prove the behaviour in Tinker or a test: 1. Register `LeadExportBuilder` with `$this->app->bind(LeadExportBuilder::class)` in a provider's `register()`. 2. Call `app(LeadExportBuilder::class)` twice and compare with `===`: the results differ, because the container honours `bind()`. 3. Call `LeadExport::getFacadeRoot()` twice and compare: the results are identical, because the second call reads `$resolvedInstance`. 4. Call `LeadExport::clearResolvedInstance()` and resolve again: now a new object appears. The experiment separates the two layers cleanly: the container's lifetime rule and the facade's own reuse are independent, and only the second one explains what the facade returns. ## Choosing a fix - Keep services behind facades **stateless**, or register them with `singleton()` so the facade's reuse matches the declared lifetime. - For a genuinely per-use object, prefer injection or an explicit `app()` call so each use is visible. - Use `$cached = false` only for facades whose every call is meant to be independent, as with `Schema` and `Pipeline`. - Long-lived application servers add their own reset rules for singletons and facades; that runtime is a separate topic.
- After a facade has resolved, you call app()->instance() with a replacement object. Which object does the facade use next, and why?It keeps using the old object. `instance()` updates the container's instance table, but the facade reads its own static `$resolvedInstance` map first and never asks the container again. Clearing that entry with the facade's `clearResolvedInstance()` makes the next static call resolve the replacement.
- Why do Laravel's Schema and Pipeline facades set $cached to false?Each call should start from a fresh object. `Schema` builds a schema builder for the default connection per call, and `Pipeline` must not carry pipes or a passable from one use into the next, so storing the first instance would leak state between unrelated calls.
saying these in an interview costs you the question
- A bind() binding guarantees a new object on every facade call
- Facades never store the object they resolve
- app()->instance() immediately changes what an already-resolved facade returns
- A queue worker keeps facade roots from the previous job
- Setting $cached to false turns the binding into a singleton