Ревью Laravel: жирные контроллеры и что писать в замечаниях
Контроллер на 300 строк с валидацией, транзакцией и отправкой письма проходит тесты, но ломается при первом изменении. Разбираем, что искать на ревью и как формулировать замечания.

Пул-реквест на пятьсот строк. Один метод store() в контроллере: валидация запроса, три запроса к базе внутри цикла, транзакция, отправка письма, запись в лог, формирование ответа. Тесты зелёные, фича работает. Через месяц приходит задача «добавить SMS вместе с письмом» — и правку приходится делать в четырёх местах, потому что тот же код скопирован в update() и в консольную команду. Это не гипотетическая проблема, а типичный результат ревью, где смотрели «работает ли», а не «как это менять дальше».
Разбираем, что искать в контроллерах Laravel (актуально для 10.x и 11.x) и как формулировать замечания так, чтобы автор понял, что именно переписать, а не обиделся на «слишком много кода в методе».
Что считать жирным контроллером
«Жирный» — не про число строк само по себе. Метод на 40 строк с плоской последовательностью может быть нормальным, а на 20 — уже проблемным, если в них смешаны три уровня ответственности. Признаки, которые видно на ревью сразу:
- в теле метода — бизнес-правила: расчёт скидки, проверка остатков, начисление бонусов;
- несколько обращений к разным моделям и построение запросов прямо в экшене;
- работа с внешними сервисами: отправка письма, вызов платёжного API, запись в очередь;
- ветвления по ролям и статусам, которые растут с каждой новой фичей;
- формирование структуры ответа руками через
response()->json([...])с ручной сборкой полей.
Контроллер в Laravel отвечает за одно: принять HTTP-запрос, делегировать работу и вернуть HTTP-ответ. Всё, что не про HTTP, в нём быть не должно. На этом и строятся замечания.
Хорошее замечание на ревью называет не «плохо», а «что переедет и куда». «Валидацию — в FormRequest, бизнес-логику — в сервис или action, сборку ответа — в API Resource» автор может выполнить за полчаса. «Тут слишком много всего» он проигнорирует.
Четыре слоя, по которым растаскивается контроллер
В экосистеме Laravel под каждую ответственность есть штатный инструмент. Ревьюер должен знать их и называть по имени.
Валидация — FormRequest
Проверка входных данных внутри метода через $request->validate([...]) терпима на прототипе, но на ревью боевого кода это повод переселить правила в отдельный класс. FormRequest выносит правила, авторизацию и подготовку данных из контроллера.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
// право на создание заказа проверяем здесь, а не в контроллере
return $this->user()->can('create', Order::class);
}
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1'],
'items.*.id' => ['required', 'integer', 'exists:products,id'],
'items.*.qty' => ['required', 'integer', 'min:1'],
'promo_code' => ['nullable', 'string', 'max:32'],
];
}
}После этого сигнатура метода принимает StoreOrderRequest $request, и провалидированные данные достаются через $request->validated(). В контроллере не остаётся ни одного правила.
Бизнес-логика — сервис или action
Расчёты, транзакции и координация нескольких моделей уезжают в отдельный класс. Laravel не навязывает конкретный паттерн — можно использовать service-класс с несколькими методами или single-action класс с одним публичным методом. Второй вариант удобен тем, что его нельзя раздуть незаметно: один класс — одна операция.
<?php
namespace App\Actions;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class CreateOrder
{
public function __construct(
private PriceCalculator $prices,
) {}
public function execute(int $userId, array $items, ?string $promo): Order
{
return DB::transaction(function () use ($userId, $items, $promo) {
$total = $this->prices->calculate($items, $promo);
$order = Order::create([
'user_id' => $userId,
'total' => $total,
]);
$order->items()->createMany($items);
return $order;
});
}
}Транзакция теперь в одном месте. Расчёт цены — в PriceCalculator, который тестируется без HTTP и без базы. Отправку письма из этого action выносим отдельно — про это ниже.
Побочные эффекты — события и очереди
Отправка письма, уведомление в мессенджер, пересчёт кэша — это не задача экшена создания заказа. Если такой код висит прямо в контроллере или даже в action синхронно, замечание на ревью: «отправка письма блокирует ответ и падает вместе с ним; вынести в событие с очередью».
// после создания заказа — событие, а не прямой вызов Mail::send()
OrderCreated::dispatch($order);Слушатель реализует ShouldQueue, и письмо уходит в фоне. Провал отправки больше не роняет создание заказа.
Формирование ответа — API Resource
Ручная сборка массива для JSON — источник расхождений между эндпоинтами: в одном месте поле называется created_at, в другом createdAt. API Resource фиксирует форму ответа в одном классе.
Как выглядит контроллер после разбора
Метод сжимается до делегирования. Читается сверху вниз без переключения контекста.
public function store(StoreOrderRequest $request, CreateOrder $createOrder)
{
$order = $createOrder->execute(
userId: $request->user()->id,
items: $request->validated('items'),
promo: $request->validated('promo_code'),
);
return new OrderResource($order);
}Четыре строки вместо трёхсот. Каждый вынесенный кусок теперь тестируется отдельно, а изменение «добавить SMS» правится в одном слушателе.
Формулировки замечаний, которые работают
Сравнение бесполезных и рабочих реплик на ревью:
| Бесполезно | Рабочее замечание |
|---|---|
| Слишком жирный контроллер | Валидацию (строки 14–28) перенести в StoreOrderRequest, тогда метод примет типизированный запрос |
| Тут много логики | Расчёт total и транзакцию вынести в action CreateOrder — сейчас это дублируется в update() |
| Письмо тут не место | Mail::send() блокирует ответ и упадёт вместе с транзакцией. Заменить на событие OrderCreated с ShouldQueue-слушателем |
| Ответ некрасивый | Ручной json-массив разъедется с GET-эндпоинтом. Собрать через OrderResource, поля будут в одном месте |
Где не надо перегибать
Растаскивание на слои — не самоцель. Три места, где рефакторинг ради рефакторинга вредит:
- CRUD без логики. Ресурсный контроллер, который просто создаёт и отдаёт запись, не нуждается в отдельном action. FormRequest и Resource достаточно.
- Action на одну строку. Класс
CreateOrder, внутри которого единственныйOrder::create()без транзакции и расчётов, — лишний уровень. Выносить есть смысл, когда там появляется координация. - Сервис-god-object. Переезд из жирного контроллера в жирный
OrderServiceна 800 строк ничего не лечит. Смотрите, чтобы у вынесенного класса была одна ответственность, а не «всё про заказы».
На ревью полезно спросить у себя: этот класс будут переиспользовать или тестировать отдельно? Если нет — вынос может не окупиться.
Что делать дальше
Начните с одного контроллера, где чаще всего конфликты в git и правки. Прогоните его по чек-листу: валидация → FormRequest, логика → action, эффекты → события, ответ → Resource. Настройте статический анализ — PHPStan с расширением Larastan ловит часть проблем ещё до ревью и снимает с ревьюера рутину. Договоритесь в команде о лимите: если метод контроллера дорос до определённого размера или содержит транзакцию — это триггер обсуждения на ревью, а не повод для придирки.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Laravel Documentation — Controllers, Form Request Validation, Eloquent API Resources, Larastan — PHPStan для Laravel (GitHub)
Частые вопросы
Action или сервис — что выбрать для бизнес-логики?
У меня Laravel 9, а не 10 или 11 — приёмы применимы?
Вынести письмо в очередь, но у нас нет настроенной очереди — что делать?
Не сработало: FormRequest не валидирует, данные проходят пустыми
Стоит ли выносить логику из контроллера, если проект маленький и один разработчик?
Как поймать жирные контроллеры автоматически, а не глазами на ревью?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.