<? phpukraine СПІВБЕСІДИ
Пошук по платформі
LARAVEL · JUNIOR ЧАСТО ПИТАЮТЬ

Фасади проти впровадження залежностей у Laravel: коли що доречне?

Фасад це статичний фасад над контейнером: `__callStatic` бере обʼєкт із контейнера за рядковим акцесором і викликає на ньому звичайний метод, тому `swap()`, `shouldReceive()` і всі `fake()` працюють. У Laravel-склейці (контролери, middleware, роути, Blade) фасад економить друкування, а в доменному коді залежності мають бути в конструкторі, бо саме сигнатура класу є списком того, що йому потрібно.

Що насправді відбувається, коли ви пишете `Cache::get('key')`? Це ж не статичний метод класу `Cache`?
У тесті стоїть `Queue::fake()`, а job усе одно виконався по-справжньому. Чому?
У сервісі шість фасадів і код працює. Чим це погано?
Що таке real-time facade і чим `use Facades\App\Services\Publisher;` відрізняється від звичайного `use`?
фасади DI service container тестування real-time facades

У класі 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()` це `__callStatic` → `getFacadeRoot()` → `resolveFacadeInstance('cache')` → `static::$app['cache']`, тобто той самий `make()` з контейнера, а не статичний стан класу.
Що корінь фасаду кешується в `Facade::$resolvedInstance`, і саме тому підміна (`swap()`, `fake()`, `shouldReceive()`) має статися до того, як залежність уперше резолвнули.
Що фейки фасадів доводять факт виклику, а не коректність job, mailable чи листа: їх треба тестувати окремо.
Що в доменному коді фасад ховає залежність від читача, від статичного аналізу й від тесту, який не піднімає застосунок.
Що real-time фасад це згенерований у `storage/framework/cache/facade-*.php` клас, чий акцесор дорівнює імені вашого класу, і його теж можна мокати через `shouldReceive()`.
Казати «фасад це статика, підмінити не можна». Підміняється легко: `Queue::fake()` кладе `QueueFake` у контейнер через `Facade::swap()`.
Шукати `Cache::fake()` або `DB::fake()`. Таких методів немає: кеш у тестах переводять на драйвер `array` або беруть `Cache::spy()`, базу тестують справжню.
Ставити `Event::fake()` без аргументів там, де в моделі є обсервер: події моделей теж заглушені, slug чи uuid не проставляються, і тест падає далеко від причини. Рятує `Event::fake([OrderShipped::class])` або `fakeExcept()`.
Лишати `Auth::user()` у доменному сервісі. Той самий код у черзі, в консольній команді чи в seeder-і отримає `null`, бо сесії там немає; ідентичність користувача треба передавати параметром.
Тягнути фасад у тест із теки `tests/Unit`, де застосунок не піднято: «A facade root has not been set».
Замінювати фасад на `app(Foo::class)` всередині методу й вважати це впровадженням залежностей. Це той самий прихований service locator, лише багатослівніший.
ПОРАДА

Почніть з механіки одним реченням: `Cache::get()` дорівнює `app('cache')->get()`, бо `__callStatic` дістає обʼєкт із контейнера за акцесором. Далі проведіть межу не між «добре і погано», а між шарами: у контролері, middleware, роутах і Blade фасад це нормальна Laravel-мова, у доменному сервісі залежності мають стояти в конструкторі. Закінчіть тим, як це перевіряється автоматично: arch-тест, який забороняє `Illuminate\Support\Facades` у теці домену.

оновлено 9 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

У `Illuminate\Support\Facades\Cache` немає жодного зі своїх методів: є лише `getFacadeAccessor()`, який повертає `'cache'`. Решту робить `__callStatic` у базовому `Facade`: `getFacadeRoot()` → `resolveFacadeInstance('cache')` → `static::$app['cache']`, і результат лягає в `Facade::$resolvedInstance`. Саме тому `Facade::swap()` (а через нього і всі `fake()`) підміняє прив'язку в контейнері й чистить цей кеш. Real-time фасад не оминає контейнер, а навпаки: його згенерований акцесор повертає імʼя вашого класу, який контейнер збирає з усіма залежностями. А `Queue::fake()` бачить лише те, що йде через чергу: `dispatchSync()` і job без `ShouldQueue` виконуються по-справжньому, їх ловить `Bus::fake()`.