- Published on
Akcje w Laravelu – Porządkowanie logiki biznesowej
Wprowadzenie
W aplikacjach Laravel jednym z kluczowych elementów jest obsługa logiki biznesowej – często jednak trudno zdecydować, w którym miejscu umieścić tę logikę. Wiele prostych projektów decyduje się na Serwisy (Service) – klasy z setkami linii kodu, które próbują robić wszystko: od zapisu do bazy danych, przez obsługę zdarzeń, aż po kalkulacje i wysyłkę powiadomień. Problem pojawia się jednak, gdy projekt zaczyna się rozrastać, a serwisom przybywa odpowiedzialności.
Aby podnieść czytelność kodu, warto sięgnąć po koncepcję akcji (Actions). Akcje stanowią dedykowane klasy, które koncentrują się na jednej operacji biznesowej (np. utworzeniu rezerwacji, zarejestrowaniu użytkownika, wysłaniu powiadomienia). Dzięki nim możemy pozbyć się przeładowanych serwisów, a także uniknąć rozbudowanych kontrolerów. W rezultacie zyskujemy kod bardziej modularny, testowalny i łatwiejszy do zrozumienia.
Terminologia
Termin „akcje” (Actions) można różnie interpretować. W kontekście Laravel rozumiemy je jako:
- Pojedynczą klasę realizującą jedno konkretne zadanie: np. utworzenie rezerwacji, zaktualizowanie statusu zamówienia czy obliczenie prowizji.
- Metodę publiczną, zwykle
execute()
, która przyjmuje dane wejściowe (np. data object, id modelu) i zwraca wynik w przewidywalnej postaci. - Miejsce na logikę, która w standardowym podejściu byłaby upakowana w kontrolerach lub serwisach.
Wiele osób rozważa używanie metod __invoke()
w tego rodzaju klasach. Czasami jednak prowadzi to do zawiłości składniowych (zwłaszcza podczas tworzenia akcji wstrzykiwanych w inne akcje) ($this->createPdf)($booking)
. Dlatego popularną konwencją jest metoda execute()
– jest ona wyraźna w wywołaniu i przyjemna w czytaniu.
Nazewnictwo Często stosuje się konwencję nazewniczą, w której nazwę klasy akcji kończymy słowem Action
. Na przykład: CreateBookingAction
, SendNotificationAction
. Pomaga to odróżnić akcje od innych elementów (kontrolerów, modeli, jobów, itp.).
Przykład użycia w praktyce
Załóżmy, że potrzebujemy operacji tworzenia rezerwacji. W tradycyjnym podejściu moglibyśmy umieścić całą logikę w kontrolerze lub serwisie BookingService
. Przy większych projektach kaskadowo rosnąca ilość kodu w serwisach czy kontrolerach staje się trudna w utrzymaniu.
Zamiast tego, stwórzmy dedykowaną klasę akcji:
namespace App\Actions;
use App\Data\BookingData;
use App\Models\Booking;
class CreateBookingAction
{
public function execute(BookingData $bookingData): Booking
{
// 1. Utworzenie nowej rezerwacji w bazie
$booking = Booking::create([
'customer_id' => $bookingData->customer_id,
'total_price' => $bookingData->total_price,
// … inne potrzebne pola
]);
// 2. Wywołanie dalszej logiki biznesowej:
// np. obliczanie podatku, generowanie numeru rezerwacji
// (może to być osobna akcja, np. CalculateTaxAction)
// 3. Zwrócenie nowo utworzonej rezerwacji
return $booking;
}
}
W kontrolerze możemy teraz zminimalizować liczbę linii do:
namespace App\Http\Controllers;
use App\Actions\CreateBookingAction;
use App\Data\BookingData;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class BookingController extends Controller
{
public function store(Request $request, CreateBookingAction $createBooking): JsonResponse
{
// Walidacja i/lub utworzenie DTO z danych requestu
$bookingData = new BookingData(...$request->validated());
// Wywołanie akcji
$booking = $createBooking->execute($bookingData);
// Zwrócenie odpowiedzi
return response()->json($booking, 201);
}
}
Dzięki temu każda klasa ma jasno określoną rolę:
- Kontroler – odpowiada za przyjęcie i walidację danych, a także za zwrócenie poprawnej odpowiedzi.
- Akcja (
CreateBookingAction
) – skupia się wyłącznie na tworzeniu rezerwacji oraz czynnościach ściśle z tym związanych. - Model – pozostaje relatywnie szczupły, skupiając się na relacjach i enkapsulacji danych.
Komponowanie akcji
W wielu przypadkach duże procesy biznesowe da się rozbić na mniejsze kroki, które można realizować za pomocą dedykowanych akcji. Na przykład tworzenie rezerwacji może obejmować:
- Utworzenie obiektu
Booking
i ustawienie pól podstawowych (akcjaCreateBookingAction
). - Dodanie przedmiotów rezerwacji (np.
CreateBookingItemAction
). - Wygenerowanie dokumentu PDF (np.
GenerateBookingPdfAction
). - Wysłanie maila z rezerwacją (np.
SendBookingMailAction
).
Z każdej z tych akcji możemy korzystać niezależnie, a także w razie potrzeby łączyć je w bardziej złożone procesy.
Przykład akcji, która łączy działanie kilku innych:
namespace App\Actions;
use App\Actions\CreateBookingItemAction;
use App\Actions\SendBookingMailAction;
use App\Data\BookingData;
use App\Models\Booking;
class CreateFullBookingAction
{
public function __construct(
protected CreateBookingAction $createBookingAction,
protected CreateBookingItemAction $createBookingItemAction,
protected SendBookingMailAction $sendBookingMailAction
) {
//
}
public function execute(BookingData $bookingData): Booking
{
// 1. Utwórz rezerwację
$booking = $this->createBookingAction->execute($bookingData);
// 2. Dodaj przedmioty rezerwacji
foreach ($bookingData->items as $itemData) {
$this->createBookingItemAction->execute($booking, $itemData);
}
// 3. Wyślij rezerwację mailem
$this->sendBookingMailAction->execute($booking);
return $booking;
}
}
Takie podejście upraszcza logikę akcji – każda z nich ma jeden, jasno określony cel. Jeśli musimy zmodyfikować np. sposób generowania rezerwacji, wystarczy, że skupimy się na jednej konkretnej akcji, nie zaś na przepastnym kodzie modelu lub dużym kontrolerze.
Alternatywy dla akcji
Komendy i Handlery (Command Bus)
W niektórych projektach używa się Commandów i Handlerów (np. z wykorzystaniem wzorca Command Bus). Komenda (np. CreateBookingCommand
) opisuje „co” trzeba wykonać, a handler (CreateBookingCommandHandler
) – „jak” to zrobić. Takie podejście daje większą elastyczność, choć jest bardziej rozbudowane w implementacji: trzeba utrzymywać osobno komendę i handler, a czasem także sam „bus” (mechanizm przekazujący komendę do właściwego handlera).
Dla średniej wielkości projektów (i większej części aplikacji) akcje mogą być wystarczające, dając bardzo podobną organizację kodu przy mniejszej złożoności.
Systemy event-driven
Jeśli rozważasz architekturę opartą o zdarzenia (eventy), możesz pomyśleć, że akcje nie są potrzebne, bo wystarczy odpowiednio emitować i obsługiwać zdarzenia. Rzeczywiście – system „event-driven” może być ogromnym ułatwieniem w skalowaniu i komponowaniu zdarzeń. Jednak często bywa on nadmiarowy w stosunku do potrzeb standardowej aplikacji CRUD, a także utrudnia śledzenie przepływu logiki (co gdzie się wywołuje). Akcje natomiast są bardziej „bezpośrednie” – od razu wiadomo, gdzie znajduje się logika danego procesu.
Podsumowanie
Akcje w Laravel to prosty, a zarazem potężny mechanizm, pozwalający uporządkować logikę biznesową i uniknąć przeładowania modeli czy kontrolerów. Ich podstawowe zalety to:
- Czytelność – klasa akcji realizuje wyłącznie jedną operację, co ułatwia jej zrozumienie.
- Testowalność – łatwo przetestować pojedynczą akcję w oderwaniu od reszty aplikacji.
- Komponowalność – akcje można składać w większe procesy, dzielić na mniejsze bloki, wstrzykiwać przez konstruktor i używać w różnych miejscach.
- Prosta integracja z Laravel – akcje korzystają z wstrzykiwania zależności (Service Container), mogą pobierać potrzebne do działania zależności w konstruktorze.
NOTE
Akcje to proste klasy bez abstrakcji i interfejsów. Akcja przyjmuje dane, przetwarza je i zwraca wynik, zwykle mając jedną publiczną metodę i czasem konstruktor. Katalog Actions opowiada "user stories", od razu widać co aplikacja robi.
To wszystko sprawia, że tworzenie i utrzymanie rozbudowanych aplikacji staje się prostsze, a kod – bardziej odporny na rozrost i nadmiar odpowiedzialności w jednym miejscu. Jeśli zależy Ci na długofalowej czytelności i stabilności Twojej aplikacji, warto wziąć akcje pod uwagę już na początku projektu (lub powoli wprowadzać je w istniejących aplikacjach).