kamilkozak.dev
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ędzy Models, Jobs i Listeners.

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) lub App/Console (dla komend CLI).

  • Przykład: InvoiceController odbiera żądanie HTTP, mapuje dane na DTO (Data Transfer Object) i wywołuje CreateInvoiceAction.

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 jak InvoicePaid, 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ąć?

  1. Wybierz jedną domenę (np. Invoices).
  2. Przenieś jego logikę do Domain/Invoices.
  3. 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.