OpenAPI¶
Пространство имён Keel\OpenApi. Маршруты, DTO из #[FromQuery] и #[FromBody], правила
валидации, #[Status], типы возврата, #[Auth] и #[RateLimit] уже описывают API; генератор
превращает их в документ OpenAPI 3.1 без второго описания, которое пришлось бы синхронизировать.
Отдача документа¶
Добавьте OpenApiMiddleware в HttpConfig и, по желанию, config/openapi.php:
return new OpenApiConfig(
title: 'Orders API',
version: '2.3.0',
description: 'Внутреннее управление заказами',
servers: [['url' => 'https://api.example.com']],
specPath: '/openapi.json', // null отключает
docsPath: '/docs', // HTML-справочник (Scalar) поверх спеки; null отключает
problemDetails: true, // документировать ответы 400/401/403/429 Problem Details
docsScript: OpenApiConfig::DEFAULT_DOCS_SCRIPT, // скрипт Scalar, который грузит страница docs; закреплён на версии
);
Документ генерируется при первом запросе и живёт, пока живёт процесс: php-fpm и встроенный
сервер пересобирают его на каждый запрос, воркеры — при перезапуске (keel:serve --watch).
keel:openapi печатает тот же JSON, keel:openapi --output=public/openapi.json записывает в файл
для коммита или генераторов клиентов.
Оба пути отвечают до роутинга, так что #[Auth] их не защитит; ограничьте доступ на обратном
прокси или отключите их в продакшене, если API не публичный. Страница docs грузит интерфейс
Scalar с jsDelivr, закреплённый на версии; через docsScript укажите другую версию или свою копию.
Что выводится¶
| Источник | В документе |
|---|---|
путь маршрута /users/{id:\d+} |
путь /users/{id}, обязательный path-параметр с типом из аргумента хэндлера, pattern из ограничения; опциональные сегменты (/users[/{id}]) дают по пути на вариант, каждый со своими параметрами; маршруты #[Redirect] не попадают |
#[FromQuery] Filter $f |
по query-параметру на каждый параметр конструктора DTO; обязателен, если нет default и тип не nullable |
#[FromQuery] int $page = 1 |
один query-параметр |
#[FromBody] CreateUser $c |
requestBody со ссылкой $ref на схему компонента CreateUser |
| параметр конструктора DTO | свойство с типом из сигнатуры: скаляры, nullable (type: [string, null]), union (oneOf), backed и unit enum (enum), вложенные DTO ($ref), DateTimeInterface (date-time), массивы с типом элементов из @param list<Item> или #[Field(items:)] |
| правило валидации | Length → minLength/maxLength, Range → minimum/maximum, Count → minItems/maxItems, Pattern/Alnum → pattern, Email → format: email, Url → format: uri, Uuid → format: uuid, Hostname → format: hostname, Ip(4)/Ip(6) → format: ipv4/ipv6, Date → format: date или date-time, Choice → enum, Unique → uniqueItems, Each → правила элементов в items, NotBlank → required |
#[Status(201)] и тип возврата |
успешный ответ: DTO → $ref, list<Dto> из @return → массив $ref, string → text/html, void/null → 204, ResponseInterface → без схемы |
возврат Page |
конверт пагинации (items, page, perPage, total, pages) с типом элементов из @return Page<Dto> или #[Response(type: Page::class, items: Dto::class)] |
#[Auth], #[Can] |
security со схемой каждого настроенного аутентификатора, плюс 401 (и 403 при ролях или способности) |
#[RateLimit] |
ответ 429 |
#[Deprecated] |
deprecated: true и x-sunset с датой отключения |
#[Idempotent] |
параметр заголовка Idempotency-Key (обязательный при required: true), ответы 409 и 422 |
#[BodyLimit] |
413, если задан размер, 415, если перечислены media types |
| есть тело или параметры | ответ 400 со схемой Problem |
Идентификаторы операций берутся из имени маршрута, иначе <Контроллер><Метод> (userShow);
теги — из имени класса контроллера без Controller. Идентификаторы в документе уникальны:
маршрут с несколькими HTTP-методами получает по одному на метод (syncGet, syncPost), а второй
маршрут, который повторил бы идентификатор (два UserController::show() в разных пространствах
имён), — числовой суффикс (userShow2). Если сгенерированные имена важны генератору клиента,
задайте им явный #[Operation(operationId:)]. Типы элементов из @param list<Item> и
@return list<Item> разрешаются как в PHP: полное имя, затем импорты use файла (включая
алиасы), затем пространство имён объявления.
Уточнения¶
#[Prefix('/users')]
#[Operation(tags: ['Users'], description: 'Учётные записи')] // умолчания для класса
final class UserController
{
#[Get('/{id}')]
#[Operation(summary: 'Получить пользователя', operationId: 'getUser')]
#[Response(200, 'Пользователь', UserView::class)]
#[Response(404, 'Нет такого пользователя')]
public function show(int $id): ResponseInterface { /* ... */ }
#[Get('/export')]
#[Operation(deprecated: true)]
public function export(): string { /* ... */ }
#[Get('/internal')]
#[Hidden] // не попадает в документ
public function internal(): array { /* ... */ }
}
final readonly class CreateUser
{
/** @param list<Address> $addresses */
public function __construct(
#[NotBlank, Length(max: 80)]
#[Field(description: 'Отображаемое имя', example: 'Ann')]
public string $name,
#[Field(items: Address::class)] // либо @param выше
public array $addresses = [],
#[Field(format: 'uuid', deprecated: true)]
public ?string $legacyId = null,
) {}
}
#[Operation]: summary, description, tags, operationId, deprecated. На классе задаёт умолчания для всех маршрутов; значения на методе побеждают по каждому полю.#[Response(status, description, type, items, contentType)], повторяемый: объявленные ответы накладываются на выведенные, так что один#[Response(404)]сохраняет выведенный 200; любой объявленный 2xx заменяет выведенный успешный ответ.type— класс DTO, имя скалярного типа илиarray.#[Field]на параметре конструктора DTO или скаляре#[FromQuery]: описание, пример, формат, тип элементов массива, deprecated.#[Hidden]на методе или классе исключает маршруты.
SchemaBuilder и Generator публичны: можно сгенерировать документ для части маршрутов
(new Generator(Router::fromControllers(...))) или строить схемы для своих инструментов.