Pracuję z Laravelem od lat i widziałem wiele. Widziałem kod elegancki, prosty i widziałem kod, który przypominał miskę spaghetti. I wiecie co? U zarania tego drugiego bardzo często stała jedna, zwodniczo prosta koncepcja: Fasady.

Tak, wiem. Są wygodne. Cache::get(), Storage::put(), Mail::send() – te wywołania wyglądają czysto i oszczędzają kilka linijek kodu. Ale ta wygoda to syreni śpiew, który prowadzi projekty prosto na skały problemów z utrzymaniem, testowaniem i skalowaniem. Dziś, jako deweloper, który nauczył się na własnych błędach, powiem wprost: unikam Fasad jak ognia w sercu mojej aplikacji. I Wy też powinniście.

Grzech pierworodny: Ukryte zależności

To jest największy i najbardziej podstępny problem z Fasadami. Zastanówmy się nad tą klasą:

namespace App\Services;

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;

class WeatherService
{
    public function getTemperature(string $city): ?int
    {
        if (strlen($city) < 3) {
            Log::warning('Próba pobrania pogody dla zbyt krótkiego miasta.', ['city' => $city]);
            return null;
        }

        $response = Http::get('https://api.weather.com', ['city' => $city]);

        if ($response->failed()) {
            Log::error('Błąd API pogody.', ['response' => $response->body()]);
            return null;
        }

        return $response->json('temperature');
    }
}

Na pierwszy rzut oka wygląda niewinnie. Ale spójrzcie na jej “kontrakt”. Jakich zależności potrzebuje ta klasa do działania? Patrząc na sygnatury metod, nie widać żadnych. A jednak, pod spodem, ta klasa sięga po dwie globalnie dostępne usługi: Log i Http. To są ukryte zależności.

Nie wiesz, czego ta klasa potrzebuje, dopóki nie przeczytasz całego jej kodu. A teraz wyobraź sobie klasę 10x większą.

A teraz zobaczmy wersję “czystą”, opartą o wstrzykiwanie zależności (Dependency Injection):

namespace App\Services;

use Psr\Log\LoggerInterface;
use Illuminate\Http\Client\Factory as HttpFactory;
use Illuminate\Http\Client\Response;

class WeatherService
{
    private $logger;
    private $http;

    public function __construct(LoggerInterface $logger, HttpFactory $http)
    {
        $this->logger = $logger;
        $this->http = $http;
    }

    public function getTemperature(string $city): ?int
    {
        if (strlen($city) < 3) {
            $this->logger->warning('Próba pobrania pogody dla zbyt krótkiego miasta.', ['city' => $city]);
            return null;
        }

        $response = $this->http->get('https://api.weather.com', ['city' => $city]);

        if ($response->failed()) {
            $this->logger->error('Błąd API pogody.', ['response' => $response->body()]);
            return null;
        }

        return $response->json('temperature');
    }
}

Różnica jest fundamentalna. Teraz kontrakt klasy (jej konstruktor) jasno i wyraźnie krzyczy, czego potrzebuje do życia. Każdy, kto będzie chciał użyć WeatherService, od razu wie, że musi dostarczyć logger i klienta HTTP. Zero magii, zero zgadywania.

Jawne gwałcenie zasad SOLID

Fasady ułatwiają łamanie dobrych praktyk. Najbardziej cierpi na tym zasada Dependency Inversion Principle (D) z akronimu SOLID. Mówi ona, że:

Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji.

Używając Log::info(), Twój kod zależy bezpośrednio od konkretnej implementacji fasady Log w Laravelu. Nie zależy od abstrakcji (np. interfejsu LoggerInterface zgodnego z PSR-3). To wiąże Twój kod na sztywno z frameworkiem. A co jeśli jutro zechcesz użyć tej logiki w innym kontekście, np. w skrypcie konsolowym, który nie bootuje całej aplikacji Laravela? Powodzenia.

Iluzja łatwego testowania

Zwolennicy Fasad powiedzą: “Ale przecież Cache::shouldReceive() jest takie proste!”. Owszem, jest. Ale to tylko plaster na ranę, którą sami sobie zadaliśmy. Mockowanie Fasad to tak naprawdę mockowanie metod statycznych, co w świecie programowania obiektowego jest uznawane za antywzorzec.

Spójrzmy na test dla naszej klasy WeatherService w wersji z DI:

public function test_returns_null_on_api_failure()
{
    // Przygotowanie mocków
    $logger = $this->createMock(LoggerInterface::class);
    $httpFactory = $this->createMock(HttpFactory::class);
    $response = $this->createMock(Response::class);

    // Definiowanie zachowania mocków
    $httpFactory->method('get')->willReturn($response);
    $response->method('failed')->willReturn(true); // Symulujemy błąd API
    $logger->expects($this->once())->method('error'); // Oczekujemy, że error zostanie zalogowany

    // Wywołanie
    $service = new WeatherService($logger, $httpFactory);
    $result = $service->getTemperature('Warszawa');

    // Asercje
    $this->assertNull($result);
}

Ten test jest całkowicie odizolowany od frameworka. Nie używa żadnej “magii” Laravela. Testuje czystą logikę mojej klasy, wstrzykując jej fałszywe implementacje zależności. Mogę ten test uruchomić w dowolnym runnerze testów zgodnym z PHPUnit, nawet bez Laravela. To jest prawdziwa elastyczność.

Podsumowanie: Gdzie jest miejsce dla Fasad?

Czy to znaczy, że Fasady są absolutnie bezużyteczne? Nie. Mają swoje miejsce, ale jest ono bardzo ograniczone. Używam ich czasem w:

  • Plikach routes/web.php lub routes/api.php: Do szybkiego definiowania tras.
  • Kontrolerach: Czasem, dla prostych, jednorazowych akcji, które nie zawierają kluczowej logiki biznesowej.
  • Poleceniach Artisan: Podobnie jak w kontrolerach.

Jednak w serwisach, modelach, repozytoriach, klasach domenowych – czyli w całym sercu aplikacji – Fasady mają absolutny zakaz wstępu.

Wybór należy do Ciebie. Możesz wybrać pozorną wygodę i pisać kod, który działa “teraz”, ale będzie koszmarem w utrzymaniu. Albo możesz wybrać dyscyplinę, pisać kod oparty o jawne zależności i budować aplikacje, które przetrwają próbę czasu. Ja swoją decyzję podjąłem dawno temu.