lepszy kod w laravelu

Lepszy kod w Laravelu: jak naprawić architekturę Active Record i nie oszaleć

Jak pisać lepszy kod w Laravelu? Poznaj błędy Active Record, zasady czystej architektury i dowiedz się, jak skutecznie odizolować Eloquent od logiki biznesowej.

Wejście w istniejący, rozwijany przez lata projekt bywa brutalnym zderzeniem z rzeczywistością. Ostatnio miałem okazję dołączyć do zespołu pracującego nad systemem opartym o framework Laravel. Choć narzędzie to pozwala na błyskawiczne dostarczanie pierwszych wersji oprogramowania, to w dużej skali ujawnia architektoniczne pułapki, które potrafią zamienić codzienną pracę w koszmar utrzymaniowy. Poniżej dzielę się kilkoma spostrzeżeniami oraz konkretnymi wskazówkami, jak pisać lepszy kod w świecie zdominowanym przez wzorzec Active Record.

Ekosystem szybkiego prototypu vs. lepszy kod w laravelu

Laravel to jeden z najpopularniejszych frameworków w świecie PHP. Jego fundamentem w warstwie danych jest Eloquent ORM, czyli klasyczna implementacja wzorca Active Record. W tym podejściu pojedynczy model odpowiada bezpośrednio wierszowi w bazie danych, łącząc w sobie strukturę tabeli, mechanizmy zapytań SQL oraz logikę biznesową.

Główna bolączka tego rozwiązania polega na gigantycznym przeroście publicznego interfejsu modeli bazowych. Klasyczny model Eloquent na starcie udostępnia setki metod powiązanych z mechanizmami bazy danych, relacjami, mutatorami czy eventami. Kiedy do jednej klasy zaczynamy dokładać procesy biznesowe, walidację i zapytania, model zamienia się w obiekt wiedzący i robiący wszystko. Zmiana jednej linijki kodu w tak splecionym organizmie staje się ryzykowna, ponieważ konsekwencje mogą ujawnić się w najmniej oczekiwanym miejscu aplikacji.

Iluzja unikania powtórzeń

Częstym błędem programistów jest opaczne rozumienie zasady Don’t Repeat Yourself. Przeświadczenie, że każdy fragment danych dotyczący użytkownika musi znaleźć się w jednej klasie modelu, prowadzi do powstawania monstrualnych struktur. Skoro mamy już tabelę z danymi klienta, najprościej jest dopisywać do niej kolejne kolumny powiązane z procesem płatności, statusem weryfikacji czy licznikami wysłanych powiadomień upominawczych.

Wydaje się, że w ten sposób kod jest zorganizowany w jednym miejscu, jednak w rzeczywistości łamiemy fundamentalną regułę pojedynczej odpowiedzialności. Poniższy kod obrazuje zanieczyszczenie modelu danymi procesu, które zmuszają klasę do obsługi zbyt wielu ról jednocześnie.

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

final class User extends Model
{
    protected $casts = [
        'verification_status' => 'string',
        'last_reminder_sent_at' => 'datetime',
        'failed_payment_attempts' => 'integer',
        'next_retry_scheduled_at' => 'datetime',
    ];
}

Rozwiązaniem tego problemu jest tworzenie małych, wyspecjalizowanych jednostek kodu, które posiadają tylko jeden powód do modyfikacji. Zamiast rozbudowywać centralny model o logikę procesową, powinniśmy wydzielać odrębne klasy zarządzające konkretnym przepływem pracy, pozostawiając modelom wyłącznie rolę kontenerów na dane. Przepływ procesu dunningowego przenosimy do dedykowanej klasy biznesowej.

declare(strict_types=1);

namespace App\Services;

use App\Repositories\UserRepository;

final class DunningProcessService
{
    private UserRepository $repository;

    public function __construct(UserRepository $repository)
    {
        $this->repository = $repository;
    }

    public function handleFailure(int $userId): void
    {
        $user = $this->repository->find($userId);
        
        if ($user === null) {
            return;
        }

        $user->incrementPaymentFailures();
        $this->repository->save($user);
    }
}

Łańcuchy powiązań i łamanie prawa Demeter

Kolejną pułapką, w którą łatwo wpaść z powodu wygody oferowanej przez relacje Eloquent, jest głębokie sprzęganie obiektów. Możliwość wywołania w jednym wierszu kodu sekwencji przechodzącej przez cztery granice biznesowe drastycznie obniża stabilność systemu. Wyobraźmy sobie sytuację, w której pobieramy zamówienie, przechodzimy do użytkownika, stamtąd do faktur, a na końcu do pojedynczych pozycji tej faktury.

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Order;

final class OrderController
{
    public function show(int $id): array
    {
        return Order::findOrFail($id)
           ->user
           ->invoices()
           ->latest()
           ->first()
           ->lineItems
           ->toArray();
    }
}

Taki zapis sprawia, że kod w module zamówień musi posiadać szczegółową wiedzę o tym, jak zbudowana jest struktura bilingowa. Każda zmiana w wewnętrznej implementacji modułu faktur natychmiast psuje logikę w miejscu wywołania zamówienia. Prawo Demeter mówi jasno, że obiekt powinien rozmawiać wyłącznie ze swoimi bezpośrednimi sąsiadami. W poprawnie zaprojektowanej architekturze moduł zamówień powinien zapytać wyspecjalizowany serwis bilingowy o konkretne dane, nie interesując się strukturą relacji bazodanowych ukrytych pod spodem.

declare(strict_types=1);

namespace App\Modules\Billing;

use App\Models\Invoice;

final class CustomerBillingQuery
{
    public function getLatestInvoiceItems(int $customerId): array
    {
        $invoice = Invoice::where('customer_id', $customerId)->latest()->first();

        if ($invoice === null) {
            return [];
        }

        return $invoice->lineItems->toArray();
    }
}

Lepszy kod w laravelu

Aby skutecznie zarządzać złożonością w dużym projekcie Laravelowym, konieczne jest odcięcie reszty aplikacji od surowego interfejsu Eloquent. Dobrą praktyką jest wdrożenie wzorca repozytorium, który staje się jedynym punktem wejścia do bazy danych, całkowicie ukrywając modele frameworka przed światem zewnętrznym. Repozytorium nie powinno zwracać obiektów Eloquent ani budowniczych zapytań, lecz mapować wyniki na czyste, niemutowalne struktury danych.

Poniższy przykład prezentuje, jak w czysty sposób odseparować model bazy danych od logiki aplikacji, stosując zasadę braku zagnieżdżeń poprzez wczesne powroty oraz całkowity zakaz używania komentarzy, gdzie intencja wynika wyłącznie z semantyki kodu.

Plik definiujący strukturę danych domeny:

declare(strict_types=1);

namespace App\Domain\Transaction;

final class Transaction
{
    public function __construct(
        public readonly int $id,
        public readonly int $userId,
        public readonly int $amount,
        public readonly string $status,
    ) {}
}

Plik z modelem bazodanowym Eloquent, stanowiący wewnętrzny detal modułu:

declare(strict_types=1);

namespace App\Repositories;

use Illuminate\Database\Eloquent\Model;

final class EloquentTransactionRecord extends Model
{
    protected $table = 'transactions';
}

Plik repozytorium realizujący jawną orkiestrację i separację:

declare(strict_types=1);

namespace App\Repositories;

use App\Domain\Transaction\Transaction;

final class TransactionRepository
{
    public function ofId(int $id): ?Transaction
    {
        $record = EloquentTransactionRecord::find($id);

        if ($record === null) {
            return null;
        }

        return $this->asEntity($record);
    }

    public function lastNotPendingOfUserId(int $userId): ?Transaction
    {
        $record = EloquentTransactionRecord::where('user_id', $userId)
            ->where('status', '!=', 'pending')
            ->orderBy('id', 'desc')
            ->first();

        if ($record === null) {
            return null;
        }

        return $this->asEntity($record);
    }

    private function asEntity(EloquentTransactionRecord $record): Transaction
    {
        return new Transaction(
            $record->id,
            $record->user_id,
            $record->amount,
            $record->status
        );
    }
}

Dzięki takiemu podziałowi warstwa biznesowa aplikacji operuje wyłącznie na bezpiecznych obiektach typu Transaction. Zewnętrzny kod traci techniczną możliwość przypadkowego wywołania metod modyfikujących bazę danych bezpośrednio na pobranej encji. Ukrycie modelu Eloquent pozwala na swobodny refaktoring tabel i zapytań wewnątrz repozytorium bez obawy o naruszenie stabilności pozostałych modułów systemu.

Architektura modułowa jako fundament efektywnej pracy z AI

Wprowadzenie jasnych granic domenowych, małych klas oraz silnego typowania niesie za sobą jeszcze jedną, niezwykle istotną we współczesnym programowaniu korzyść. Drastycznie zwiększa efektywność generowania i refaktoryzacji kodu przez modele językowe. Praca z LLM wewnątrz monolitu opartego na klasycznym Active Record bywa bezowocna, ponieważ sztuczna inteligencja potrzebuje ogromnego kontekstu całego systemu, aby przewidzieć efekty uboczne modyfikacji jednej metody w grubym modelu. Wielotysięczne klasy przepełnione ukrytymi callbackami przekraczają okna kontekstowe i prowadzą do halucynacji.

Stosowanie małych jednostek o jednej odpowiedzialności i zdefiniowanych interfejsach odwraca tę sytuację. Model LLM otrzymuje jako kontekst wyłącznie jedno, kilkudziesięcioliniowe repozytorium lub izolowany serwis biznesowy. Mając przed sobą czysty, jednoznaczny kod bez głębokich zagnieżdżeń i ukrytych zależności frameworka, sztuczna inteligencja potrafi wygenerować bezbłędne, gotowe do wdrożenia produkcyjnego rozwiązania. Modularność i czytelna struktura to najlepszy sposób na to, aby uczynić z AI precyzyjnego partnera, a nie źródło losowych błędów w architekturze.

Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również