kamilkozak.dev
Published on

Domain Driven Laravel: Struktura i Organizacja Kodu w Dużych Aplikacjach

Wprowadzenie

W aplikacjach Laravel zazwyczaj porządkujemy kod według typów plików: kontrolery do jednego folderu, modele do drugiego, a pliki widoków i migracje do kolejnych. Takie podejście doskonale sprawdza się w mniejszych projektach i prototypach. Jednak wraz ze wzrostem złożoności aplikacji pojawia się problem: coraz trudniej odnaleźć miejsca, w których zachodzi logika biznesowa, a kod rozprasza się w wielu katalogach.

Domain Driven Laravel to pragmatyczne podejście inspirowane DDD (Domain Driven Design), ale bez ślepego trzymania się teorii. Chodzi o to, aby grupować funkcjonalności w sposób bardziej zbliżony do realnych potrzeb biznesowych, niż do struktury technicznej frameworka. Dzięki temu aplikacja staje się bardziej czytelna, a zarazem łatwiejsza w utrzymaniu.

Czym są domeny?

W kontekście programowania termin „domena” odnosi się do konkretnego obszaru problemowego, który dana aplikacja ma rozwiązać. Jeżeli tworzymy na przykład system rezerwacji hotelowych, to poszczególne domeny mogą obejmować takie zagadnienia, jak:

  • Rezerwacje (informacje o terminach, gościach, pokojach)
  • Fakturowanie (wystawianie i obsługa faktur)
  • Płatności (proces płatności, integracje z bramkami płatniczymi)
  • Obsługa gości (klienci, programy lojalnościowe, historia pobytów)

Zamiast rozdzielać kod według rodzaju plików — np. wszystkie kontrolery w app/Http/Controllers, a wszystkie modele w app/Models — proponuje umieścić wszystkie elementy związane z konkretną domeną we wspólnym folderze o nazwie odzwierciedlającej tę domenę.

Dlaczego warto?

1. Lepsze odwzorowanie procesów biznesowych

Osoby nieznające kodu (np. nowi członkowie zespołu) łatwiej odnajdą miejsce, w którym należy wprowadzić zmiany. Jeśli klient poprosi o modyfikację funkcjonalności związanej z fakturowaniem, wszystkie niezbędne pliki — modele, akcje (metody obsługujące logikę biznesową), zasady walidacji, eventy itp. — znajdują się w jednym miejscu.

2. Mniejszy „próg wejścia” dla nowych osób

W większych projektach kod rośnie w szybkim tempie. Struktura oparta na domenach pomaga skrócić czas potrzebny na odnalezienie i zrozumienie konkretnej części logiki, ponieważ nowa osoba nie musi przekopywać się przez wiele osobnych katalogów.

3. Jasna separacja warstw

Laravel oferuje mnóstwo udogodnień (Eloquent, migracje, komendy Artisan itp.). Koncentrując kod stricte biznesowy w folderze Domain, zyskujemy też klarowną separację od warstwy dostępowej (HTTP).

Domeny a warstwa aplikacji

W podejściu "domain driven laravel" wyróżnia się dwie kluczowe warstwy:

  1. Warstwa domeny
    Skupia całą logikę biznesową: modele, klasy obsługujące przetwarzanie danych, akcje (ang. actions), job'y, eventy i powiązane z nimi listenery. Tutaj znajduje się sedno tego, co system robi.

  2. Warstwa aplikacji
    Dotyczy interakcji użytkownika lub innego systemu z naszą domeną. Mogą to być kontrolery HTTP lub CLI (komendy Artisan). Ta warstwa nie powinna zawierać rozbudowanej logiki — jej zadanie to wywołanie odpowiednich metod w domenie oraz zwrócenie odpowiedzi.

W typowym projekcie Laravel folder app/Http (zawierający kontrolery, middleware’y i requesty) stanowi przykład warstwy aplikacji. Z kolei folder app/Domain (lub src/Domain jeśli zdecydujemy się stworzyć go poza app/) gromadzi wszystko, co należy do domen.

Domeny w praktyce

Przykładowa struktura katalogów może wyglądać następująco:

app
├── Http
│   ├── Controllers
│   ├── Middleware
│   ├── Requests
│   └── Resources
└── Domain
    ├── Bookings
    │   ├── Actions
    │   ├── Jobs
    │   ├── Models
    │   ├── QueryBuilders
    │   └── Services
    ├── Customers
    │   ├── Actions
    │   ├── Events
    │   ├── Models
    │   └── ...
    └── Invoices
        ├── Actions
        ├── Events
        ├── Models
        ├── Rules
        └── ...

Wewnątrz każdego folderu domeny umieszczamy wszystkie elementy ściśle związane z tą domeną — np. Actions do operacji takich jak „utwórz rezerwację” czy „anuluj rezerwację”; Models, które reprezentują tabele w bazie danych; Jobs do zadań kolejkowanych itp.

Dopasowanie do istniejących konwencji

Proponuję nie walczyć z frameworkiem i pozostać blisko domyślnej struktury Laravel, możesz trzymać logikę domen w folderze app/Domain, a resztę kodu pozostawić tak, jak w świeżej instalacji frameworka (app/Http, app/Console itd.). Ważne jest, byś znalazł balans między preferencjami zespołu a przejrzystością aplikacji.

Nazewnictwo i podział domen

Podział na domeny wynika z realnych potrzeb biznesowych. Czasem początkowe grupowanie może z czasem wymagać rozbicia na mniejsze moduły lub scalenia kilku obszarów w jeden. Staraj się, by nazwy odzwierciedlały to, co naprawdę dzieje się w projekcie (np. Invoices, Payments, Orders), zamiast skupiać się na technikaliach (np. Services, Utils).

Konfiguracja niestandardowych ścieżek

Autoload w pliku composer.json

Jeśli zdecydujesz się wydzielić domeny poza folder app, dodaj odpowiedni wpis w sekcji autoload:

{
  "autoload": {
    "psr-4": {
      "App\\": "app/",
      "Domain\\": "src/Domain/"
    }
  }
}

Potem uruchom composer dump-autoload. Dzięki temu autoloader będzie wiedział, gdzie szukać Twoich klas domenowych.

NOTE

Nie komplikuj struktury na siłę. Jeśli projekt jest mały, wystarczy podstawowy katalog app/Domain. Rozbudowuj architekturę tylko wtedy, gdy coś na tym zyskujesz.

Współpraca między warstwami

Po wydzieleniu logiki do folderów domenowych, kontrolery i inne elementy warstwy aplikacji (commandy) wywołują odpowiednie klasy domeny. Na przykład w kontrolerze możesz mieć metodę store, w której tworzysz nową fakturę:

use Domain\Invoices\Actions\CreateInvoiceAction;
use Domain\Invoices\DataTransferObjects\InvoiceData;

class InvoiceController extends Controller
{
    public function store(Request $request, CreateInvoiceAction $createInvoiceAction): InvoiceResource
    {
        $invoiceData = InvoiceData::fromRequest($request);

        $invoice = $createInvoiceAction->execute($invoiceData);

        return new InvoiceResource($invoice);
    }
}

Model Invoice oraz klasa akcji CreateInvoiceAction znajdują się w folderze src/Domain/Invoices. Kontroler InvoiceController pozostaje natomiast w app/Http/Controllers.

Podsumowanie

Projektowanie aplikacji „zorientowanych na domeny” pozwala lepiej odwzorować rzeczywiste procesy biznesowe i uniknąć chaosu rosnącego wraz z rozbudową kodu. Dzięki temu:

  1. Łatwiej się odnaleźć — kod dotyczący konkretnego problemu (np. fakturowanie) jest zgromadzony w jednym miejscu.
  2. Łatwiej wprowadzać zmiany — wprowadzając nową funkcjonalność, wiesz dokładnie, gdzie powinna się znaleźć.
  3. Kod jest bardziej przejrzysty — rozdzielasz wyraźnie logikę domenową od warstwy aplikacji (HTTP, CLI), która tylko deleguje (orkiestruje) zadania do domeny.

NOTE

Wskazówka: Pamiętaj, że proponowana struktura nie jest „jedynym słusznym rozwiązaniem”. Każdy projekt ma swoją specyfikę — warto przeanalizować rozmiar i złożoność aplikacji, a także preferencje zespołu, by wybrać najlepsze rozwiązania.