Основы веб-разработки

Контроллеры

Контроллер принимает HTTP-запрос и возвращает ответ. В Winter это класс, наследующий стереотип Controller; методы с атрибутами маршрутов — обработчики. Сам класс намеренно «пустой»: зависимости приходят через контейнер, а всё поведение задаётся атрибутами.

База Stereotype\ControllerСоздаётся контейнером (DI)Возвращает Sendable

Что такое контроллер и зачем

Контроллер — это граница между HTTP и вашим приложением: точка, куда приходит запрос и откуда уходит ответ.

Проблема. Логике приложения нельзя напрямую зависеть от HTTP: если разбор запроса, вызов бизнес-логики и формирование ответа перемешаны в одном месте, код тяжело тестировать и переиспользовать (ту же операцию не вызвать из консоли или джобы). Решение — тонкий слой-обработчик: контроллер принимает запрос, делегирует работу сервису и возвращает ответ, не смешивая транспорт с логикой.

В Winter Controller — это стереотип: базовый класс с чёткой ролью «вход HTTP». Он минимален — по сути маркер, по которому сканер находит обработчики, а контейнер знает, как построить экземпляр:

php
namespace Flytachi\Winter\K2\Stereotype;

abstract class Controller implements ControllerInterface
{
  final public function __construct() {}
}

Отсюда два следствия:

  • Вы не пишете конструктор с аргументами руками — он final и пустой. Зависимости внедряются через #[Autowired]-свойства или через контейнер.
  • Вы не вызываете new UserController() — контроллер создаёт фреймворк на каждый запрос, разрешая все зависимости.

Создание

Сгенерируйте контроллер командой make (суффикс Controller добавится сам):

bash
php call make -c .User   # → main/UserController.php

Каркас минимален — класс, стереотип и один метод-обработчик:

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]

Самый частый вариант: типизированное свойство с атрибутом. Контейнер заполнит его при создании контроллера:

main/UserController.php
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:

php
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):

php
// Ресурс внутри макета: 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

Готовые фабрики под частые форматы:

php
return ResponseFile::json($payload);           // JSON-файл
return ResponseFile::csv($rows, 'report.csv');  // CSV-выгрузка
return ResponseFile::binary($bytes, $name);     // произвольные байты
return ResponseFile::file($absolutePath);       // файл с диска

Тонкий контроллер

Контроллер — это вход, а не место для бизнес-логики. Держите его тонким: принял запрос, делегировал сервису, вернул ответ. Логика — в Service, доступ к данным — в Repository:

php
#[PostMapping]
public function create(#[RequestJson, Valid] CreateUserRequest $req): ResponseEntity
{
  // никакой логики здесь — только оркестрация
  $user = $this->service->register($req);
  return ResponseEntity::created($user);
}

Почему так

Тонкие контроллеры проще тестировать и переиспользовать: одну и ту же логику сервиса можно вызвать из контроллера, консольной команды или джобы. Разделение Controller → Service → Repository — базовая модель Winter (см. Ключевые понятия).

Middleware на контроллере

Пред- и постобработку (аутентификацию, логирование, проверку прав) подключают атрибутом middleware — на классе (для всех методов) или на отдельном методе:

php
#[AuthMiddleware]                 // на весь контроллер
#[RequestMapping('admin')]
class AdminController extends Controller
{
  #[AuthMiddleware]             // или только на конкретный метод
  #[GetMapping('stats')]
  public function stats(): ResponseEntity { /* ... */ }
}

Как писать своё middleware (before() / after()) и в каком порядке они выполняются — на странице Middleware.


Под капотом

Как фреймворк превращает входящий запрос в вызов метода контроллера. Для повседневной работы знать это не обязательно.

На каждый совпавший маршрут происходит следующее:

text
1. Контейнер создаёт контроллер
 → все #[Autowired]-зависимости разрешаются и внедряются
2. Аргументы метода наполняет ParameterResolver
 → #[PathVariable] / #[RequestJson] / #[RequestQuery] … по метаданным
 → метаданные берутся из ReflectionCache (рефлексия кешируется, не повторяется)
3. Метод вызывается, возвращает Sendable
4. Sendable::send() сериализует результат в HTTP-ответ

Ключевые моменты:

  • Экземпляр — на запрос. Контроллер не разделяется между запросами, поэтому свойства безопасно наполнять данными текущего запроса.
  • Рефлексия кешируется. ReflectionCache хранит разобранные сигнатуры методов — привязка параметров не платит за рефлексию на каждый запрос.
  • Возврат — это Sendable. ResponseEntity, ResponseView и ResponseFile реализуют общий интерфейс send(HttpResponse, HttpRequest), поэтому контроллер не знает про транспорт (FPM/Swoole) — он просто возвращает объект ответа.

Дальше