Контроллеры
Контроллер принимает HTTP-запрос и возвращает ответ. В Winter это класс,
наследующий стереотип Controller; методы с атрибутами маршрутов — обработчики.
Сам класс намеренно «пустой»: зависимости приходят через контейнер, а всё
поведение задаётся атрибутами.
Что такое контроллер и зачем
Контроллер — это граница между HTTP и вашим приложением: точка, куда приходит запрос и откуда уходит ответ.
Проблема. Логике приложения нельзя напрямую зависеть от HTTP: если разбор запроса, вызов бизнес-логики и формирование ответа перемешаны в одном месте, код тяжело тестировать и переиспользовать (ту же операцию не вызвать из консоли или джобы). Решение — тонкий слой-обработчик: контроллер принимает запрос, делегирует работу сервису и возвращает ответ, не смешивая транспорт с логикой.
В Winter Controller — это стереотип: базовый класс с чёткой ролью «вход HTTP». Он
минимален — по сути маркер, по которому сканер находит обработчики, а контейнер
знает, как построить экземпляр:
namespace Flytachi\Winter\K2\Stereotype;
abstract class Controller implements ControllerInterface
{
final public function __construct() {}
}Отсюда два следствия:
- Вы не пишете конструктор с аргументами руками — он
finalи пустой. Зависимости внедряются через#[Autowired]-свойства или через контейнер. - Вы не вызываете
new UserController()— контроллер создаёт фреймворк на каждый запрос, разрешая все зависимости.
Создание
Сгенерируйте контроллер командой make (суффикс Controller добавится сам):
php call make -c .User # → main/UserController.phpКаркас минимален — класс, стереотип и один метод-обработчик:
<?php
namespace Main;
use Flytachi\Winter\K2\Http\Response\ResponseEntity;
use Flytachi\Winter\K2\Route\Annotation\GetMapping;
use Flytachi\Winter\K2\Route\Annotation\RequestMapping;
use Flytachi\Winter\K2\Stereotype\Controller;
#[RequestMapping('users')]
class UserController extends Controller
{
#[GetMapping]
public function index(): ResponseEntity
{
return ResponseEntity::ok([]);
}
}Дальше добавляете методы с атрибутами маршрутов — см. Маршрутизацию.
Зависимости
Контроллеру почти всегда нужны сервисы и репозитории. Внедряйте их — не создавайте
через new. Два способа:
Свойство с #[Autowired]
Самый частый вариант: типизированное свойство с атрибутом. Контейнер заполнит его при создании контроллера:
use Flytachi\Winter\DI\Attribute\Autowired;
#[RequestMapping('users')]
class UserController extends Controller
{
#[Autowired] private UserService $service;
#[Autowired] private LoggerInterface $logger;
#[GetMapping('{id}')]
public function get(#[PathVariable] int $id): ResponseEntity
{
$this->logger->info('fetch user', ['id' => $id]);
return ResponseEntity::ok($this->service->find($id));
}
}Логгер по типу
#[Autowired] LoggerInterface $logger даёт логгер, автоматически именованный по
классу-потребителю — без getLogger(self::class). Механика — в
доках logger.
Подробно о жизненных циклах, ручных привязках и #[Lazy] — на странице
Внедрение зависимостей.
Возврат ответа
Метод-обработчик возвращает объект, реализующий Sendable. Тип объекта задаёт
формат ответа:
| Возвращаете | Для чего | Content-Type |
|---|---|---|
ResponseEntity |
Данные / JSON-API | application/json |
ResponseView |
Серверный HTML (шаблоны) | text/html |
ResponseFile |
Файлы, выгрузки, потоки | по типу файла |
Данные — ResponseEntity
Основной тип для API. Фабрики задают HTTP-код, тело сериализуется в JSON:
return ResponseEntity::ok($data); // 200
return ResponseEntity::created($data); // 201
return ResponseEntity::accepted($data); // 202
return ResponseEntity::noContent(); // 204
return ResponseEntity::badRequest($err); // 400
return ResponseEntity::notFound(); // 404
// Дополнительный заголовок — builder-стилем:
return ResponseEntity::ok($data)->header('X-Total-Count', (string) $total);HTML — ResponseView
Для серверного рендеринга шаблонов из resources/. view() рендерит ресурс без
макета, render() — оборачивает ресурс в макет (внутри макета доступен $content):
// Ресурс внутри макета: resources/layouts/main.php + resources/users/index.php
return ResponseView::render('layouts/main', 'users/index', ['users' => $users]);
// Только ресурс, без макета:
return ResponseView::view('users/row', ['user' => $user]);Файлы — ResponseFile
Готовые фабрики под частые форматы:
return ResponseFile::json($payload); // JSON-файл
return ResponseFile::csv($rows, 'report.csv'); // CSV-выгрузка
return ResponseFile::binary($bytes, $name); // произвольные байты
return ResponseFile::file($absolutePath); // файл с дискаТонкий контроллер
Контроллер — это вход, а не место для бизнес-логики. Держите его тонким: принял
запрос, делегировал сервису, вернул ответ. Логика — в Service, доступ к данным —
в Repository:
#[PostMapping]
public function create(#[RequestJson, Valid] CreateUserRequest $req): ResponseEntity
{
// никакой логики здесь — только оркестрация
$user = $this->service->register($req);
return ResponseEntity::created($user);
}Почему так
Тонкие контроллеры проще тестировать и переиспользовать: одну и ту же логику
сервиса можно вызвать из контроллера, консольной команды или джобы. Разделение
Controller → Service → Repository — базовая модель Winter (см.
Ключевые понятия).
Middleware на контроллере
Пред- и постобработку (аутентификацию, логирование, проверку прав) подключают атрибутом middleware — на классе (для всех методов) или на отдельном методе:
#[AuthMiddleware] // на весь контроллер
#[RequestMapping('admin')]
class AdminController extends Controller
{
#[AuthMiddleware] // или только на конкретный метод
#[GetMapping('stats')]
public function stats(): ResponseEntity { /* ... */ }
}Как писать своё middleware (before() / after()) и в каком порядке они
выполняются — на странице Middleware.
Под капотом
Как фреймворк превращает входящий запрос в вызов метода контроллера. Для повседневной работы знать это не обязательно.
На каждый совпавший маршрут происходит следующее:
1. Контейнер создаёт контроллер
→ все #[Autowired]-зависимости разрешаются и внедряются
2. Аргументы метода наполняет ParameterResolver
→ #[PathVariable] / #[RequestJson] / #[RequestQuery] … по метаданным
→ метаданные берутся из ReflectionCache (рефлексия кешируется, не повторяется)
3. Метод вызывается, возвращает Sendable
4. Sendable::send() сериализует результат в HTTP-ответКлючевые моменты:
- Экземпляр — на запрос. Контроллер не разделяется между запросами, поэтому свойства безопасно наполнять данными текущего запроса.
- Рефлексия кешируется.
ReflectionCacheхранит разобранные сигнатуры методов — привязка параметров не платит за рефлексию на каждый запрос. - Возврат — это
Sendable.ResponseEntity,ResponseViewиResponseFileреализуют общий интерфейсsend(HttpResponse, HttpRequest), поэтому контроллер не знает про транспорт (FPM/Swoole) — он просто возвращает объект ответа.
Дальше
- Запросы и ответы — привязка входных данных и форматы ответов
- Внедрение зависимостей — как наполняется
#[Autowired] - Middleware — пред- и постобработка запроса