У класі Illuminate\Support\Facades\Cache методу get() немає. Єдине, що там оголошено, це захищений getFacadeAccessor() з рядком 'cache'. Решту бере на себе базовий Facade через __callStatic: він викликає getFacadeRoot(), той іде в resolveFacadeInstance('cache'), а той дістає обʼєкт із контейнера виразом static::$app['cache'] і запамʼятовує його в статичному масиві Facade::$resolvedInstance. Далі на цьому обʼєкті викликається звичайний нестатичний метод. Тому Cache::get('key') це просто інший спосіб написати app('cache')->get('key'); статичного стану тут немає, якщо не рахувати кеш уже розвʼязаних коренів. Посилання на контейнер потрапляє у фасади під час бутстрапу: RegisterFacades викликає Facade::setFacadeApplication($app) і заразом реєструє псевдоніми через AliasLoader, тому в namespaced-файлі можна писати \Cache і потрапляти в Illuminate\Support\Facades\Cache. Ще одна деталь механіки: акцесор веде до менеджера, а не до кінцевого обʼєкта. 'cache' це CacheManager, який своїм __call перекидає виклик на store() для драйвера за замовчуванням, 'queue' це QueueManager. На практиці це означає, що драйвер підбирається за конфігом у момент першого виклику, а не тоді, коли ви пишете код.
Тестованість фасадів росте з тієї самої механіки. Facade::swap($instance) кладе обʼєкт у контейнер і чистить $resolvedInstance; Facade::shouldReceive('get') створює Mockery-мок і робить те саме. Усі знайомі фейки це обгортки над swap: Queue::fake(), Bus::fake(), Event::fake(), Mail::fake(), Notification::fake(), Storage::fake('public'), Http::fake([...]). Кожен підставляє клас із суфіксом Fake, який замість справжньої дії записує виклик і додає асерти на кшталт assertPushed(), assertSent(), assertDispatchedSync(). Підступів тут два. Фейк діє лише на те, що резолвиться після нього, тому підміна після того, як контролер уже отримав залежність у конструктор, нічого не змінить. І Event::fake() без списку класів глушить також події моделей, тому обсервер, який проставляє slug, мовчки не спрацює, а тест впаде за кілька кроків від справжньої причини. Корисно тримати в голові й те, чого не існує: Cache::fake() і DB::fake() немає, кеш у тестах переводять на драйвер array (або беруть Cache::spy() для перевірки викликів), а базу тестують справжню з RefreshDatabase. І межа всіх фейків одна: Queue::assertPushed(WarmSitemap::class) доводить, що job поставили в чергу, і нічого не каже про те, чи він працює правильно.
У доменному коді фасад коштує іншого: залежність зникає із сигнатури. Ні статика, ні продуктивність тут ні до чого. Клас із Cache::remember(), Auth::user() і Log::info() всередині методів виглядає як обʼєкт без залежностей, хоча йому потрібен піднятий застосунок, налаштований кеш і активна сесія. Створити такий клас оператором new у тесті не вийде: будь-який фасад у теці tests/Unit, де застосунок не бутстрапиться, впаде з «A facade root has not been set». Перевикористати його в іншому контексті теж не вийде, бо Auth::user() у черзі поверне null: воркер не має сесії. І другу реалізацію йому не даси, бо контекстна привʼязка when()->needs()->give() працює по типах параметрів конструктора, а параметрів тут немає. Аргумент про читабельність спрацьовує у зворотний бік: сім залежностей у конструкторі виглядають незручно і саме тому корисні, вони чесно показують, що клас робить забагато, тоді як сім фасадів у тілі методів той самий факт ховають. Конструктор до того ж дає статичному аналізу типи, а не рядкові ключі контейнера.
Між цими двома полюсами є незручна середина, і на неї розраховані real-time facades. Якщо написати use Facades\App\Services\Publisher; і далі викликати Publisher::publish($post), автозавантажувач псевдонімів побачить префікс Facades\, згенерує клас-нащадок Facade, чий getFacadeAccessor() повертає App\Services\Publisher, і збереже його у файл storage/framework/cache/facade-*.php. Ваш клас при цьому лишається звичайним: контейнер збирає його з усіма залежностями конструктора, а в тесті він мокається рядком Facades\App\Services\Publisher::shouldReceive('publish')->once(). Рятує це там, де конструктора немає взагалі: Blade-директива, старий хелпер, статичний контекст у legacy-коді. Приховану залежність real-time фасад не прибирає, він лише робить її тестованою, тож у новому коді конструктор усе одно виграє.
Плюс на співбесіді дає вміння провести межу по шарах застосунку, а не по особистих смаках. Контролер, middleware, файл роутів, Blade-шаблон, консольна команда, міграція це і є Laravel-склейка: там фасади доречні, читаються звично й нічого не ховають, бо ці класи й так не існують поза фреймворком. Доменні сервіси, value objects, калькулятори тарифів і правила публікації живуть у власному шарі, і туди залежності приходять конструктором: інтерфейс репозиторію, власний Clock замість now(), скаляр із конфігу замість config(). У Laravel 11+ останнє скорочується атрибутами контейнера: #[Config('services.stripe.key')] string $key у конструкторі дає значення без згадки фасаду, поруч є #[Auth], #[DB], #[Log], #[Storage], #[CurrentUser]. Домовленість краще закріпити машиною, а не код-рев'ю: arch-тест expect('App\Domain')->not->toUse('Illuminate\Support\Facades') падає в CI на першому порушенні. Окремо про довгі процеси: Facade::$resolvedInstance це статичний кеш, і під Octane, де застосунок живе між запитами, фреймворк скидає його разом із підміною контейнера на sandbox, тому проблеми зі станом там породжують не фасади, а ваші власні singleton-и, що памʼятають дані запиту.
// Доменний сервіс: жодного Illuminate, усі залежності видно в конструкторі
final class PublishPost
{
public function __construct(
private readonly PostRepository $posts, // інтерфейс доменного шару
private readonly Clock $clock, // власна абстракція часу
) {}
public function __invoke(PostId $id): void
{
$post = $this->posts->byId($id);
$post->publish($this->clock->now());
$this->posts->save($post);
}
}
// Шар фреймворку: тут фасад доречний, це і є Laravel-склейка
final class PublishPostController
{
public function __invoke(string $id, PublishPost $publish): RedirectResponse
{
$publish(new PostId($id)); // контейнер зібрав сервіс за type-hint
Cache::forget('posts.published'); // акцесор 'cache' -> app('cache')
Queue::push(new WarmSitemap); // акцесор 'queue' -> app('queue')
return back()->with('status', 'Опубліковано');
}
}
it('ставить прогрів мапи в чергу', function () {
Queue::fake(); // під капотом Facade::swap(new QueueFake(...))
// Доменну залежність підміняє контейнер, а не фасад
app()->instance(Clock::class, new FrozenClock('2026-09-09 10:00:00'));
$this->post('/posts/17/publish')->assertRedirect();
Queue::assertPushed(WarmSitemap::class); // факт постановки, не поведінка job
});
Почніть з механіки одним реченням: `Cache::get()` дорівнює `app('cache')->get()`, бо `__callStatic` дістає обʼєкт із контейнера за акцесором. Далі проведіть межу не між «добре і погано», а між шарами: у контролері, middleware, роутах і Blade фасад це нормальна Laravel-мова, у доменному сервісі залежності мають стояти в конструкторі. Закінчіть тим, як це перевіряється автоматично: arch-тест, який забороняє `Illuminate\Support\Facades` у теці домену.