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

Запросы и привязка параметров

Winter сам наполняет аргументы метода данными запроса. Вы объявляете, что нужно — тип и атрибут источника — а фреймворк читает запрос, приводит значение к типу и передаёт его в метод. Никаких $_GET, $_POST и json_decode в коде контроллера.

Атрибуты Request\AnnotationРезолвер ParameterResolverПриведение к типу аргумента

Что такое привязка параметров и зачем

Привязка параметров (parameter binding) — это автоматическое преобразование сырого HTTP-запроса в готовые, типизированные аргументы метода.

Проблема. Данные из HTTP приходят строками и лежат в разных местах: часть в пути URL, часть в query-строке, часть в теле (JSON, форма или XML), что-то в заголовках. Разбирать их вручную ($_GET['page'], json_decode(...)), проверять наличие, приводить "42" к int — это рутина, которая повторяется в каждом методе и легко даёт ошибки.

Решение. Опишите параметр типом и атрибутом источника — остальное сделает фреймворк: найдёт значение в нужном месте запроса, приведёт к объявленному типу и передаст в метод (а при несоответствии сам вернёт 400). Об этом и раздел.

Как это работает

Когда маршрут найден, перед вызовом метода отрабатывает ParameterResolver. Он идёт по параметрам метода по порядку и для каждого:

  1. читает тип и атрибут;
  2. находит источник (путь / query / тело / заголовок / файл);
  3. проверяет наличие (обязателен ли);
  4. приводит к объявленному PHP-типу;
  5. подставляет в аргумент.
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] берут одиночное значение и приводят его к типу аргумента:

php
#[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 {}

Имя параметра по умолчанию совпадает с именем аргумента; для заголовков имя приводится автоматически (authorizationAuthorization).

Приведение типов

Значение приходит строкой и приводится к объявленному типу. При несоответствии — 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, а цель — по типу аргумента:

php
#[PostMapping('orders')]
public function create(#[RequestBody] CreateOrderDto $dto): ResponseEntity
{
  // JSON/form/XML-тело разобрано и разложено в поля DTO
}

DTO — обычный класс с типизированными свойствами конструктора. Недостающие или несовпадающие по типу поля собираются в ошибку 400 (все сразу).

Принудительный формат

Когда клиент присылает неверный Content-Type или формат нужно зафиксировать, используйте явные атрибуты вместо авто-определения:

php
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, путь):

php
#[PostMapping('avatar')]
public function upload(#[RequestFile('file')] array $file): ResponseEntity
{
  // $file — данные загруженного поля "file"
}

Query как DTO

Когда параметров фильтра много, удобнее собрать всю query-строку в один объект через #[RequestQuery] — вместо десятка отдельных #[RequestParam]:

php
#[GetMapping('orders')]
public function list(#[RequestQuery] OrderFilter $filter): ResponseEntity
{
  // ?page=2&limit=50&status=active → поля $filter
}

#[RequestQuery] всегда опционален — отсутствующие поля берут значения по умолчанию.

Обязательные и опциональные

Параметр из HTTP-источника обязателен по умолчанию. Он становится необязательным, если добавить значение по умолчанию или сделать тип nullable:

php
?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 без атрибута, фреймворк внедрит их сам:

php
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');
  // ...
}

Под капотом

Как резолвер выбирает источник для каждого параметра. Для повседневной работы знать не обязательно.

Порядок приоритета

Резолвер проверяет правила в фиксированном порядке — побеждает первое совпадение:

text
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 до любого приведения — ошибка конфигурации видна при разработке, а не в рантайме:

php
// ✗ LogicException:
public function a(#[RequestParam] int|string $value): void {}
// ✓ один тип:
public function b(#[RequestParam] string $value): void {}

Дальше