Запросы и привязка параметров
Winter сам наполняет аргументы метода данными запроса. Вы объявляете, что
нужно — тип и атрибут источника — а фреймворк читает запрос, приводит значение к
типу и передаёт его в метод. Никаких $_GET, $_POST и json_decode в коде
контроллера.
Что такое привязка параметров и зачем
Привязка параметров (parameter binding) — это автоматическое преобразование сырого HTTP-запроса в готовые, типизированные аргументы метода.
Проблема. Данные из HTTP приходят строками и лежат в разных местах: часть в
пути URL, часть в query-строке, часть в теле (JSON, форма или XML), что-то в
заголовках. Разбирать их вручную ($_GET['page'], json_decode(...)), проверять
наличие, приводить "42" к int — это рутина, которая повторяется в каждом методе
и легко даёт ошибки.
Решение. Опишите параметр типом и атрибутом источника — остальное сделает
фреймворк: найдёт значение в нужном месте запроса, приведёт к объявленному типу
и передаст в метод (а при несоответствии сам вернёт 400). Об этом и раздел.
Как это работает
Когда маршрут найден, перед вызовом метода отрабатывает ParameterResolver. Он
идёт по параметрам метода по порядку и для каждого:
- читает тип и атрибут;
- находит источник (путь / query / тело / заголовок / файл);
- проверяет наличие (обязателен ли);
- приводит к объявленному PHP-типу;
- подставляет в аргумент.
#[GetMapping('orders/{id:\d+}')]
public function show(
#[PathVariable] int $id, // из пути
#[RequestParam] int $page = 1, // из query ?page=
#[RequestHeader] string $authorization, // из заголовка
): ResponseEntity {
// всё уже разобрано и приведено к типам
}Источники данных
Каждому месту в запросе — свой атрибут (все в Flytachi\Winter\K2\Http\Request\Annotation):
| Атрибут | Откуда берёт | Цель |
|---|---|---|
#[PathVariable] |
Сегмент пути URL — /users/{id} |
Скаляр |
#[RequestParam] |
Один параметр query — ?key=val |
Скаляр |
#[RequestQuery] |
Всю query-строку целиком | DTO |
#[RequestHeader] |
HTTP-заголовок запроса | Скаляр |
#[RequestBody] |
Тело (формат определяется автоматически) | Строка / массив / объект / DTO |
#[RequestJson] |
Тело — принудительно как JSON | array / stdClass / DTO |
#[RequestForm] |
Тело — принудительно как форма | array / stdClass / DTO |
#[RequestXml] |
Тело — принудительно как XML | array / stdClass / DTO |
#[RequestFile] |
Загруженный файл (multipart) | array (данные файла) |
Скалярные источники
#[PathVariable], #[RequestParam] и #[RequestHeader] берут одиночное значение
и приводят его к типу аргумента:
#[GetMapping('users/{id}')]
public function get(
#[PathVariable] int $id, // /users/42 → 42
#[RequestParam] ?string $search = null, // ?search=foo → "foo"
#[RequestParam] bool $active = false, // ?active=true → true
#[RequestParam] array $ids = [], // ?ids[]=1&ids[]=2 → [1, 2]
#[RequestHeader] string $authorization, // заголовок Authorization
): ResponseEntity {}Имя параметра по умолчанию совпадает с именем аргумента; для заголовков имя
приводится автоматически (authorization → Authorization).
Приведение типов
Значение приходит строкой и приводится к объявленному типу. При несоответствии —
400 Bad Request:
| PHP-тип | Пример входа | Поведение | Ошибка |
|---|---|---|---|
int |
"42" |
FILTER_VALIDATE_INT |
400 |
float |
"3.14" |
FILTER_VALIDATE_FLOAT |
400 |
bool |
true/false/1/0/yes/no/on/off |
FILTER_VALIDATE_BOOLEAN |
400 |
string |
любое | как есть | никогда |
array |
key[]=v |
должен быть массивом (скаляр отвергается) | 400 |
BackedEnum |
значение кейса enum | Enum::from() |
400 |
DateTimeImmutable / DateTime |
ISO 8601 | конструктор из строки | 400 |
BcMath\Number / Decimal\Decimal |
числовая строка | точное число из строки | 400 |
Точность чисел
BcMath\Number и Decimal\Decimal получают значение строкой, а не через
float — "1.1" остаётся "1.1" без потери точности IEEE 754.
Тело запроса
#[RequestBody] разбирает тело и наполняет аргумент. Формат определяется
автоматически по Content-Type, а цель — по типу аргумента:
#[PostMapping('orders')]
public function create(#[RequestBody] CreateOrderDto $dto): ResponseEntity
{
// JSON/form/XML-тело разобрано и разложено в поля DTO
}DTO — обычный класс с типизированными свойствами конструктора. Недостающие или
несовпадающие по типу поля собираются в ошибку 400 (все сразу).
Принудительный формат
Когда клиент присылает неверный Content-Type или формат нужно зафиксировать,
используйте явные атрибуты вместо авто-определения:
public function a(#[RequestJson] CreateOrderDto $dto): ResponseEntity {} // всегда JSON
public function b(#[RequestForm] array $form): ResponseEntity {} // всегда form
public function c(#[RequestXml] \stdClass $node): ResponseEntity {} // всегда XMLЦель может быть array, stdClass или любой DTO-класс.
Файлы
#[RequestFile] привязывает загруженный файл из multipart-запроса. Аргумент —
массив с данными файла (имя, размер, MIME, путь):
#[PostMapping('avatar')]
public function upload(#[RequestFile('file')] array $file): ResponseEntity
{
// $file — данные загруженного поля "file"
}Query как DTO
Когда параметров фильтра много, удобнее собрать всю query-строку в один объект
через #[RequestQuery] — вместо десятка отдельных #[RequestParam]:
#[GetMapping('orders')]
public function list(#[RequestQuery] OrderFilter $filter): ResponseEntity
{
// ?page=2&limit=50&status=active → поля $filter
}#[RequestQuery] всегда опционален — отсутствующие поля берут значения по умолчанию.
Обязательные и опциональные
Параметр из HTTP-источника обязателен по умолчанию. Он становится необязательным, если добавить значение по умолчанию или сделать тип nullable:
?int $page // null, если параметра нет
int $page = 1 // 1, если параметра нет
?int $page = null // null, если параметра нетПорядок при отсутствии значения: есть default → вернуть его; тип nullable →
null; иначе → 400.
Пустая строка ≠ отсутствие
?page= (пустая строка) — это присутствующее, но невалидное значение: для
int/float/bool/enum/date вернётся 400. А ?page вовсе не переданный
— берёт default или null. Исключение — тип string: пустая строка валидна.
Объекты запроса и ответа
Нужен сырой запрос или ответ — объявите аргумент типа HttpRequest /
HttpResponse без атрибута, фреймворк внедрит их сам:
use Flytachi\Winter\K2\Http\Contracts\HttpRequest;
#[PostMapping('login')]
public function login(#[RequestJson] LoginDto $dto, HttpRequest $request): ResponseEntity
{
$ip = $request->header()->get('X-Forwarded-For');
// ...
}Под капотом
Как резолвер выбирает источник для каждого параметра. Для повседневной работы знать не обязательно.
Порядок приоритета
Резолвер проверяет правила в фиксированном порядке — побеждает первое совпадение:
1. #[PathVariable] сегмент пути
2. #[RequestParam] query ?key=val
3. #[RequestBody] тело (формат по Content-Type)
4. #[RequestFile] multipart-файл
5. #[RequestJson] тело как JSON
6. #[RequestForm] тело как форма
7. #[RequestXml] тело как XML
8. #[RequestQuery] вся query как DTO
9. #[RequestHeader] заголовок
10. HttpRequest сырой объект запроса
11. HttpResponse сырой объект ответа
12. совпадение по имени сегмент пути без атрибута
13. значение по умолчанию
14. nullable-тип (?T) → nullЕсли не подошло ни одно правило — RuntimeException (некорректный контроллер,
виден на старте в Swoole-режиме).
Union-типы не поддерживаются
Объединённые и пересечённые типы на HTTP-параметрах резолвер отвергает сразу,
бросая LogicException до любого приведения — ошибка конфигурации видна при
разработке, а не в рантайме:
// ✗ LogicException:
public function a(#[RequestParam] int|string $value): void {}
// ✓ один тип:
public function b(#[RequestParam] string $value): void {}Дальше
- Валидация — проверка полей DTO атрибутами
#[Valid] - Ответы — форматы и коды ответа
- Обработка ошибок — что происходит при
400/422