- Published on
Domain Driven Laravel: Jak budować skalowalne aplikacje?
Dlaczego warto myśleć o domenach w Laravel?
Laravel doskonale radzi sobie z prostymi aplikacjami CRUD. Jednak gdy projekt rośnie – pojawiają się dziesiątki modeli, setki endpointów i złożone reguły biznesowe – klasyczna struktura frameworka (grupowanie plików według typów, np. Models
, Controllers
) staje się pułapką. Kod rozsypuje się po katalogach, a proste zmiany wymagają przeskakiwania między dziesiątkami klas.
Przykład z życia: Klient nie mówi: „Dodaj endpoint do modelu Invoice”, tylko: „Chcę generować faktury z automatycznym numerowaniem i powiadomieniami dla klienta”. Domeny pozwalają grupować kod wokół takich biznesowych celów, a nie technicznych szczegółów.
Dlaczego CRUD nie wystarcza?
- Mix logiki biznesowej i infrastruktury: Kontrolery i modele stają się „tykającymi bombami” z setkami linii kodu.
- Trudności w utrzymaniu: Gdy funkcjonalność faktur jest rozproszona po całej aplikacji, zmiana jednej reguły wymaga przeszukiwania wielu plików.
- Bariera dla nowych developerów: Zamiast skupić się na biznesie, muszą analizować, gdzie ukryto poszczególne fragmenty logiki.
Rozwiązanie: Domeny grupują wszystko, co związane z konkretnym obszarem biznesowym (np. faktury, rezerwacje, płatności). To nie tylko modele, ale także reguły walidacji, zdarzenia, akcje i specyficzne dla domeny wyjątki.
Czym jest Domain Driven Laravel?
Domain Driven Laravel to pragmatyczne podejście inspirowane DDD (Domain Driven Design), ale bez ślepego trzymania się teorii. Chodzi o to, aby:
- Organizować kod wokół biznesu, a nie frameworka.
- Wyodrębnić rdzeń aplikacji (logikę biznesową) z warstwy infrastrukturalnej (HTTP, kolejki, CLI).
- Uprościć współpracę – developer od faktur pracuje głównie w katalogu
Domain/Invoices
, nie przeskakując międzyModels
,Jobs
iListeners
.
Kluczowe elementy:
- Domeny: Foldery jak
Invoices
,Customers
,Bookings
, zawierające wszystko, co dotyczy danej funkcjonalności. - Czyste modele: Modele Eloquent są „chude” – przechowują dane, ale nie logikę biznesową (ta trafia do osobnych klas, np.
Actions
). - Warstwa aplikacyjna: Kontrolery, middleware i widoki działają jako „most” między użytkownikiem a domeną.
Przykład struktury:
app/Domain/Invoices/
├── Actions/
│ ├── CreateInvoiceAction.php
│ └── SendInvoiceReminderAction.php
├── Events/
│ └── InvoicePaid.php
├── Models/
│ └── Invoice.php
├── Rules/
│ └── ValidInvoiceNumberRule.php
└── States/
└── PaidInvoiceState.php
Domena vs. Aplikacja: Dwa światy, jeden projekt
Aby uniknąć chaosu, warto rozdzielić:
Domena (Domain):
Co? Logika biznesowa – np. generowanie numeru faktury, obliczanie terminu płatności, przyznanie rabatu.
Gdzie? W folderze
Domain/Invoices
.Przykład: Klasa
CreateInvoiceAction
sprawdza dostępność produktów, oblicza VAT i zapisuje fakturę do bazy.Aplikacja (Application):
Co? Routing, autentykacja, autoryzacja, walidacja requestu, formatowanie odpowiedzi HTTP.
Gdzie? W folderze
App/Http
(dla API) lubApp/Console
(dla komend CLI).Przykład:
InvoiceController
odbiera żądanie HTTP, mapuje dane na DTO (Data Transfer Object) i wywołujeCreateInvoiceAction
.
Dlaczego to działa?
- Izolacja zmian: Modyfikując logikę biznesową, nie dotykasz warstwy HTTP.
- Elastyczność: Możesz mieć wiele aplikacji (HTTP, CLI, API) korzystających z jednej domeny.
- Czyste API: Kontrolery są „głupie” – tylko przekazują dalej.
Struktura katalogów w praktyce
Przykład dla domeny Invoices
:
Actions/
: Operacje biznesowe, np. tworzenie faktury, wysyłka przypomnień.Events/
: Zdarzenia jakInvoicePaid
, które mogą wywołać notyfikacje lub integracje z zewnętrznymi systemami.Models/
: Modele Eloquent z relacjami i podstawowymi metodami pomocniczymi.QueryBuilders/
: Customowe konstruktory zapytań, np.Invoice::overdue()
.States/
: Implementacje wzorca State dla statusów faktury (np.PaidInvoiceState
,DraftInvoiceState
).
Kod przykładowy (model z customowym Query Builderem):
namespace Domain\Invoices\Models;
use Domain\Invoices\QueryBuilders\InvoiceQueryBuilder;
use Illuminate\Database\Query\Builder;
class Invoice extends Model
{
// Metoda wskazująca na customowy Query Builder
public function newEloquentBuilder(Builder $query): InvoiceQueryBuilder
{
return new InvoiceQueryBuilder($query);
}
}
InvoiceQueryBuilder.php
:
namespace Domain\Invoices\QueryBuilders;
use Illuminate\Database\Eloquent\Builder;
class InvoiceQueryBuilder extends Builder
{
public function overdue(): self
{
return $this->where('due_date', '<', now())
->where('status', '!=', 'paid');
}
}
Użycie w kontrolerze:
$overdueInvoices = Invoice::overdue()->get();
Konfiguracja przestrzeni nazw dla domen
Aby używać niestandardowych ścieżek (np. src/Domain
zamiast app/Domain
):
Krok 1: Zmodyfikuj sekcję autoload
w composer.json
"autoload": {
"psr-4": {
"App\\": "app/",
"Domain\\" : "src/Domain/",
...
}
},
Krok 2: Uruchom composer dump-autoload
, aby zaktualizować autoloader.
Przenieś Domain poza app
, gdy:
- Projekt ma wiele niezależnych obszarów jak
Invoices
,Bookings
,Payments
, które mogą ewoluować osobno. - Chcesz pójść krok dalej i jeszcze bardziej oddzielić domenę.
- Planujesz wydzielenie domen do odseparowanych modułów lub mikroserwisów.
NOTE
Nie komplikuj struktury na siłę. Jeśli projekt jest mały, wystarczy podstawowy katalog app/Domain
. Rozbudowuj architekturę tylko wtedy, gdy zyskujesz na tym klarowność lub wydajność.
Podsumowanie: Laravel to nie tylko CRUD'y
Domain Driven Laravel to nie rewolucja, ale ewolucja architektury, która sprawdza się w projektach średniej i dużej skali. Nie wymaga porzucania frameworka – wystarczy przemyślane grupowanie kodu.
Dlaczego warto eksperymentować?
- Nawet jeśli nie wdrożysz pełnego DDD, każda separacja logiki biznesowej od infrastruktury to zawsze dobry krok.
- Laravel jest na tyle elastyczny, że pozwala łączyć różne podejścia oraz zmieniać strukturę katalogów.
Jak zacząć?
- Wybierz jedną domenę (np.
Invoices
). - Przenieś jego logikę do
Domain/Invoices
. - Stopniowo refaktoryzuj kolejne obszary.
Pamiętaj: perfekcjonizm jest wrogiem postępu. Lepiej wprowadzać zmiany małymi krokami niż próbować przebudować cały projekt naraz.