Wzorzec Action–Domain–Responder (ADR) w praktyce
Wprowadzenie
Action–Domain–Responder (ADR) to wzorzec architektoniczny przeznaczony głównie dla aplikacji webowych. Powstał jako odpowiedź na problemy klasycznego MVC, w którym kontrolery często stają się zbyt rozbudowane, łącząc w sobie obsługę HTTP, logikę biznesową i przygotowanie odpowiedzi.
ADR skupia się na wyraźnym rozdzieleniu odpowiedzialności i bardzo dobrze sprawdza się w frameworkach, które nie narzucają ciężkiej struktury – takich jak Koseven.
Na czym polega ADR?
ADR dzieli obsługę jednego żądania HTTP na trzy jasno określone role:
- Action – obsługa żądania
- Domain – logika biznesowa
- Responder – przygotowanie odpowiedzi HTTP
Schemat przepływu:
Request → Action → Domain → Responder → Response
Każda z tych warstw ma jedno zadanie i nie wchodzi w kompetencje pozostałych.
1. Action
Action to punkt wejścia dla żądania HTTP.
Odpowiedzialności:
- pobranie danych z requestu
- wywołanie logiki domenowej
- przekazanie wyniku do Respondera
Cechy:
- brak logiki biznesowej
- brak formatowania odpowiedzi
- zazwyczaj jedna klasa = jeden use‑case
W Koseven rola Action jest naturalnie realizowana przez kontroler obsługujący jedno żądanie.
2. Domain
Domain zawiera całą logikę biznesową aplikacji.
Odpowiedzialności:
- reguły biznesowe
- walidacja domenowa
- operacje na modelach / repozytoriach
Cechy:
- brak zależności od HTTP
- brak zależności od frameworka
- łatwa do testowania
Domain zwraca wynik operacji (np. DTO) albo zgłasza wyjątek domenowy.
3. Responder
Responder odpowiada za stworzenie odpowiedzi HTTP.
Odpowiedzialności:
- wybór formatu odpowiedzi (JSON / View / Redirect)
- ustawienie statusu HTTP
- przygotowanie nagłówków
Cechy:
- nie zna requestu
- nie zawiera logiki biznesowej
ADR w czystym PHP (bez frameworka)
Zanim przejdziemy do implementacji w Koseven, warto zobaczyć jak ADR wygląda w najczystszej postaci, bez żadnego frameworka. Pozwala to lepiej zrozumieć podział odpowiedzialności i zobaczyć, że ADR nie jest zależny od narzędzi, a jedynie od architektury.
Poniższy przykład pokazuje ten sam przypadek użycia: utworzenie użytkownika.
Struktura katalogów (czyste PHP)
src/
├── Action/
│ └── CreateUserAction.php
├── Domain/
│ ├── CreateUser.php
│ └── UserDTO.php
├── Responder/
│ └── CreateUserResponder.php
└── Http/
├── Request.php
└── Response.php
Action
class CreateUserAction
{
public function __construct(
private CreateUser $domain,
private CreateUserResponder $responder
) {}
public function __invoke(Request $request): Response
{
try {
$result = $domain->handle($request->post());
return $this->responder->success($result);
} catch (DomainException $e) {
return $this->responder->error($e->getMessage());
}
}
}
Action:
- przyjmuje request
- deleguje logikę do Domain
- nie zna formatu odpowiedzi
Domain
class CreateUser
{
public function handle(array $data): UserDTO
{
if (empty($data['email'])) {
throw new DomainException('Email is required');
}
// przykładowa logika biznesowa
return new UserDTO(1, $data['name'], $data['email']);
}
}
DTO:
class UserDTO
{
public function __construct(
public int $id,
public string $name,
public string $email
) {}
}
Domain:
- nie zna HTTP
- nie zna frameworka
- zawiera wyłącznie reguły biznesowe
Responder
class CreateUserResponder
{
public function success(UserDTO $user): Response
{
return new Response(
json_encode([
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]),
201
);
}
public function error(string $message): Response
{
return new Response(
json_encode(['error' => $message]),
422
);
}
}
Responder:
- odpowiada wyłącznie za HTTP
- wybiera status i format odpowiedzi
Wniosek
Ten przykład pokazuje, że ADR jest wzorcem niezależnym od frameworka. Koseven (i każdy inny framework) pełni jedynie rolę infrastruktury, która:
- dostarcza Request i Response
- obsługuje routing
- ułatwia integrację
Z tą wiedzą przejście do implementacji ADR w Koseven staje się naturalne.
Dlaczego ADR pasuje do Koseven?
Koseven (fork Kohany) bardzo dobrze wspiera ADR, ponieważ:
- nie narzuca rozbudowanego MVC
- kontrolery są lekkie
- routing jest elastyczny
- framework nie wymusza ORM ani struktury domeny
W praktyce:
Controller w Koseven = Action w ADR
Przykładowa struktura projektu
application/
├── classes/
│ ├── Controller/
│ │ └── Action/
│ │ └── User/
│ │ └── Create.php
│ ├── Domain/
│ │ └── User/
│ │ ├── CreateUser.php
│ │ └── UserDTO.php
│ ├── Responder/
│ │ └── User/
│ │ └── CreateResponder.php
│ └── Model/
│ └── User.php
Przykład: tworzenie użytkownika (POST /users)
Action (Controller)
class Controller_Action_User_Create extends Controller
{
public function action_index()
{
$data = $this->request->post();
$domain = new Domain_User_CreateUser();
$responder = new Responder_User_CreateResponder();
try {
$result = $domain->handle($data);
$this->response = $responder->success($result);
} catch (DomainException $e) {
$this->response = $responder->error($e->getMessage());
}
}
}
Action:
- pobiera dane z requestu
- deleguje logikę do Domain
- przekazuje wynik do Respondera
Domain – logika biznesowa
class Domain_User_CreateUser
{
public function handle(array $data): Domain_User_UserDTO
{
if (empty($data['email'])) {
throw new DomainException('Email is required');
}
if (ORM::factory('User')->where('email', '=', $data['email'])->find()->loaded()) {
throw new DomainException('Email already exists');
}
$user = ORM::factory('User');
$user->values([
'name' => $data['name'],
'email' => $data['email'],
])->save();
return new Domain_User_UserDTO(
$user->id,
$user->name,
$user->email
);
}
}
DTO:
class Domain_User_UserDTO
{
public function __construct(
public int $id,
public string $name,
public string $email
) {}
}
Responder – odpowiedź HTTP
class Responder_User_CreateResponder
{
public function success(Domain_User_UserDTO $user): Response
{
return Response::factory()
->status(201)
->body(json_encode([
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]));
}
public function error(string $message): Response
{
return Response::factory()
->status(422)
->body(json_encode([
'error' => $message
]));
}
}
Routing w Koseven
Route::set('user.create', 'users')
->defaults([
'controller' => 'Action_User_Create',
'action' => 'index',
]);
ADR vs klasyczne MVC
| MVC | ADR |
|---|---|
| Kontroler robi wszystko | Action tylko koordynuje |
| Logika w kontrolerze | Logika w Domain |
| Widok w kontrolerze | Widok w Responderze |
| Trudne testy | Łatwe testy |
Zalety ADR
- czytelny podział odpowiedzialności
- brak „grubych kontrolerów”
- łatwe testowanie logiki biznesowej
- dobra skalowalność
- świetne dopasowanie do API
Kiedy warto stosować ADR?
- w dużych aplikacjach
- w API
- przy refaktoryzacji starego MVC
- w projektach z DDD lub CQRS
- gdy zależy Ci na czystej architekturze
Podsumowanie
ADR to prosty, ale bardzo skuteczny wzorzec architektoniczny. W połączeniu z Koseven pozwala budować aplikacje:
- modularne
- łatwe w utrzymaniu
- odporne na rozrost złożoności
Dzięki temu, że framework nie narzuca ciężkiej struktury, ADR w Koseven jest naturalnym i eleganckim wyborem.