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.phplubroutes/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.