Проект на Laravel начинается просто: маршрут, контроллер, модель Eloquent. Этого хватает надолго. Проблемы появляются, когда один метод контроллера валидирует данные, открывает транзакцию, списывает деньги, шлёт письмо и собирает JSON. Потом то же правило копируют в консольную команду, для теста нужен полноценный HTTP-запрос, а трогать метод никто не решается.
Паттерны архитектуры отвечают на один вопрос: в каком классе лежит какой кусок кода. Это инструменты, а не правила, и каждый стоит дополнительных файлов.
Тонкие контроллеры и DTO
Контроллер переводит HTTP в вызов и результат обратно в HTTP. Валидация и авторизация уходят в FormRequest, правило — в Action, форма ответа — в Resource.
public function store(Request $request)
{
$data = $request->validate(['items' => 'required|array']);
$order = DB::transaction(function () use ($request, $data) {
$order = Order::create(['user_id' => $request->user()->id]);
$order->items()->createMany($data['items']);
return $order;
});
Mail::to($request->user())->send(new OrderPlacedMail($order));
return response()->json($order, 201);
}Тот же эндпоинт после разделения:
public function store(
StoreOrderRequest $request,
PlaceOrder $action,
): JsonResponse {
$order = $action->handle(PlaceOrderData::fromRequest($request));
return OrderResource::make($order)
->response()
->setStatusCode(201);
}Между контроллером и Action передают DTO (data transfer object): небольшой класс с типизированными полями вместо array или всего Request.
namespace App\DataTransferObjects;
use App\Http\Requests\StoreOrderRequest;
final readonly class PlaceOrderData
{
public function __construct(
public int $userId,
public array $items,
public ?string $coupon = null,
) {}
public static function fromRequest(StoreOrderRequest $request): self
{
return new self(
userId: $request->user()->id,
items: $request->validated('items'),
coupon: $request->validated('coupon'),
);
}
}💡
readonly classтребует PHP 8.2 и новее. Laravel 12 требует PHP 8.2+, так что он всегда доступен. Про HTTP знает толькоfromRequest(), поэтому Action можно вызвать из команд, job и тестов.