Контроллеры: организация логики

Контроллер собирает связанную логику в один класс, чтобы маршруты оставались чистой картой, а код — структурированным.

Суть: вместо замыканий в web.php логику выносят в классы-контроллеры из app/Http/Controllers. Каждый метод обслуживает свой маршрут.

Писать всю логику прямо в маршрутах удобно лишь для пары страниц. Как только приложение растёт, файл web.php превращается в свалку. Контроллеры решают это: они группируют методы вокруг одной сущности. Например, ProductController содержит всё, что связано с товарами: показ списка, показ одного товара, создание, обновление, удаление.

Контроллер — это обычный PHP-класс, наследник базового Controller. Каждый его публичный метод — это действие (action), которое можно привязать к маршруту. Создаются контроллеры командой artisan, что экономит время и гарантирует правильную структуру.

Создание и привязка

# обычный контроллер
php artisan make:controller ProductController

# ресурсный контроллер со всеми CRUD-методами
php artisan make:controller ProductController --resource
<?php
namespace App\Http\Controllers;

use App\Models\Product;

class ProductController extends Controller
{
    // список товаров
    public function index()
    {
        $products = Product::all();
        return view('products.index', ['products' => $products]);
    }

    // один товар
    public function show(Product $product)
    {
        return view('products.show', ['product' => $product]);
    }
}

Привязка маршрута к контроллеру выглядит так:

<?php
use App\Http\Controllers\ProductController;

Route::get('/products',      [ProductController::class, 'index']);
Route::get('/products/{product}', [ProductController::class, 'show']);

// все CRUD-маршруты одной строкой
Route::resource('products', ProductController::class);

Как работает под капотом

Когда маршрут указывает на [ProductController::class, 'index'], Laravel создаёт экземпляр контроллера через свой контейнер зависимостей и вызывает нужный метод. Если в сигнатуре метода объявлен тип (например, Product $product), фреймворк автоматически найдёт нужную запись в базе по параметру маршрута — это называется неявное связывание моделей.

  Route::resource('products', ...) разворачивается в:

  GET    /products            -> index()   список
  GET    /products/create     -> create()  форма создания
  POST   /products            -> store()   сохранение
  GET    /products/{id}        -> show()    один товар
  GET    /products/{id}/edit   -> edit()    форма правки
  PUT    /products/{id}        -> update()  обновление
  DELETE /products/{id}        -> destroy() удаление

Семь методов из одной строки Route::resource — это и есть сила соглашений. Смоделируем диспетчеризацию «контроллер + действие» на Python.

Попробуй сам ▶

# Контроллер как набор действий
class ProductController:
    def index(self):           return 'Список товаров'
    def show(self, id):        return f'Товар {id}'
    def store(self, data):     return f'Создан: {data}'

def dispatch(controller, action, **params):
    method = getattr(controller, action)
    return method(**params)

c = ProductController()
print(dispatch(c, 'index'))
print(dispatch(c, 'show', id=7))
print(dispatch(c, 'store', data='Книга'))

Частые ошибки

  • «Толстые» контроллеры. Сотни строк бизнес-логики в одном методе — признак, что пора выносить код в сервисы.
  • Забыть импорт класса. Без use App\Http\Controllers\ProductController; маршрут не найдёт контроллер.
  • Неправильное имя параметра. Для неявного связывания имя параметра маршрута должно совпадать с именем переменной в методе.

Best practices

  • Один контроллер — одна сущность; для нетипичных действий делайте отдельный одно-методный контроллер (__invoke).
  • Используйте Route::resource для стандартного CRUD — меньше кода и единый стиль.
  • Тяжёлую логику выносите в сервис-классы, оставляя контроллер тонким координатором.

Контроллеры умеют принимать зависимости через внедрение (dependency injection). Если объявить в конструкторе или прямо в методе тип нужного класса — например, сервис OrderService $orders или объект Request $request — Laravel через контейнер сам создаст и передаст его. Это убирает ручное создание объектов и упрощает тестирование: в тесте легко подменить зависимость заглушкой. Для действий, которые не вписываются в стандартный CRUD, удобны одиночные контроллеры с магическим методом __invoke(): класс создаётся командой make:controller ProcessPayment --invokable, а в маршруте указывается просто имя класса без действия. Такой контроллер делает ровно одно дело и хорошо читается. Наконец, у ресурсных контроллеров можно ограничить набор методов через ->only([...]) или ->except([...]), если, скажем, удаление в вашем ресурсе не предусмотрено — это держит карту маршрутов честной и компактной.

Итог: контроллеры группируют логику вокруг сущностей и делают маршруты чистой картой. Вместе с ресурсными маршрутами они дают готовую структуру CRUD. Дальше посмотрим на middleware — фильтры запросов.

Проверьте себя
1. Что делает команда Route::resource('products', ProductController::class)?
AСоздаёт один маршрут index
BСоздаёт семь CRUD-маршрутов сразу
CУдаляет все маршруты товаров
DСоздаёт модель Product
2. Что такое неявное связывание моделей (route model binding)?
AРучной поиск записи в базе
BАвтоматический поиск модели по параметру маршрута через тип в сигнатуре метода
CКэширование маршрутов
DУдаление модели