Генератор OpenAPI
Lift\OpenApi\Generator рефлексує ваші контролери й виробляє специфікацію OpenAPI 3.0.3 — JSON-документ, який споживають інструменти на кшталт Swagger UI, Redoc, Postman і генератори коду. Специфікація будується з тих самих атрибутів #[Get] / #[Post] / …, які ви вже використовуєте для маршрутизації через атрибути, плюс жменьки необов’язкових атрибутів, специфічних для OpenAPI.
Ментальна модель: атрибути маршрутів описують, як під’єднати ендпоінт; атрибути OpenAPI описують, що розповісти людям і машинам про нього. Lift генерує специфікацію на етапі збірки / завантаження, ви віддаєте її як JSON.
Коли варто перейматися
- У вашого API є зовнішні споживачі (мобільний застосунок, сторонні інтеграції).
- Вам потрібна безкоштовна, завжди актуальна сторінка Swagger UI.
- Ваша команда використовує генератор коду для створення типізованих SDK.
Коли не варто: невеликий внутрішній API, де документації в README достатньо.
Приклад за 30 секунд
use Lift\OpenApi\Generator;
use Lift\OpenApi\Attribute\{ApiOperation, ApiResponse, ApiParam, ApiTag};
use Lift\Attribute\{Get, Post, Group};
#[Group('/api/v1')]
#[ApiTag('Users')]
final class UserController
{
#[Get('/users/{id:\d+}')]
#[ApiOperation(summary: 'Fetch one user')]
#[ApiResponse(200, description: 'OK')]
#[ApiResponse(404, description: 'Not found')]
public function show(Request $req): Response { /* … */ }
#[Post('/users')]
#[ApiOperation(summary: 'Create user')]
#[ApiParam('email', in: 'body', type: 'string', description: 'User email')]
#[ApiResponse(201, description: 'Created')]
public function store(Request $req): Response { /* … */ }
}
// У початковому завантаженні або CLI-команді:
$gen = new Generator(
title: 'My API',
version: '1.0.0',
description: 'JSON API for everything',
serverUrl: 'https://api.example.com',
);
$gen->addController(UserController::class);
file_put_contents(__DIR__ . '/../public/openapi.json', $gen->toJson());
Тепер Swagger UI / Redoc можуть вказувати на /openapi.json і рендерити документацію.
API генератора
$gen = new Generator(
title: 'My API', // обов’язково
version: '1.0.0', // обов’язково
description: '…', // опційно
serverUrl: 'https://api.example.com',
);
$gen->addController(UserController::class);
$gen->addController(OrderController::class);
$gen->addSchema(UserDTO::class); // для components/schemas
$gen->addSecurityScheme('bearerAuth', [
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
]);
$spec = $gen->generate(); // масив
$json = $gen->toJson(); // JSON-рядок, за замовчуванням гарний
generate() повертає специфікацію як звичайний асоціативний масив. toJson() — синтаксичний цукор над json_encode.
Атрибути OpenAPI
Усі живуть під Lift\OpenApi\Attribute\. Вони окремі від атрибутів маршрутизації — можна використовувати один набір без іншого.
На рівні класу
| Атрибут | Призначення |
|---|---|
#[ApiTag] |
Згрупувати шляхи всіх методів під тегом у документації |
#[ApiSecurity] |
Безпека за замовчуванням, застосовувана до всіх методів класу |
#[ApiTag('Users', description: 'Account management')]
#[ApiSecurity(scheme: 'bearerAuth')]
final class UserController { … }
На рівні методу
| Атрибут | Призначення |
|---|---|
#[ApiOperation] |
Summary, description, operationId, теги на метод |
#[ApiParam] |
Документувати параметр query / path / header / body |
#[ApiResponse] |
Документувати один код відповіді з необов’язковою схемою |
#[ApiSecurity] |
Перевизначити / додати безпеку для цього методу |
#[Get('/users/{id:\d+}')]
#[ApiOperation(
summary: 'Get user',
description: 'Returns one user by ID',
operationId: 'getUserById',
)]
#[ApiParam('id', in: 'path', type: 'integer', description: 'User ID')]
#[ApiParam('include', in: 'query', type: 'string', required: false)]
#[ApiResponse(200, description: 'OK', schema: UserResource::class)]
#[ApiResponse(404, description: 'Not found')]
public function show(Request $req): Response { … }
Параметри шляху, оголошені в URL, автоматично включаються навіть без #[ApiParam] — генератор витягує їх із шаблонів {id:\d+}. Обмеження через двокрапку (:\d+) автоматично вирізається під час видачі специфікації, тож ви отримуєте /users/{id}, а не буквальний regex.
Компоненти та схеми
Для складних тіл відповіді/запиту оголосіть PHP-клас DTO й посилайтеся на нього:
use Lift\OpenApi\Attribute\ApiSchema;
#[ApiSchema(name: 'User', description: 'Public user representation')]
final class UserDTO
{
public int $id;
public string $email;
public ?string $name;
public bool $active;
}
Зареєструйте й посилайтеся:
$gen->addSchema(UserDTO::class);
// У контролері:
#[ApiResponse(200, schema: UserDTO::class)]
У згенерованій специфікації відповідь стає:
{
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/User" } }
}
}
}
}
Генератор оглядає публічні властивості й зіставляє типи PHP з типами OpenAPI:
| PHP | OpenAPI |
|---|---|
int / integer |
{"type": "integer"} |
float / double |
{"type": "number", "format": "float"} |
string |
{"type": "string"} |
bool / boolean |
{"type": "boolean"} |
array |
{"type": "array", "items": {"type": "string"}} (вважайте за TODO) |
Для тоншого контролю (вкладені об’єкти, масиви посилань, переліки) передайте сиру JSON-схему як рядок:
#[ApiResponse(200, schema: '{"type":"array","items":{"$ref":"#/components/schemas/User"}}')]
Схеми безпеки
OpenAPI розділяє «які схеми існують» і «яка схема застосовується до якої операції». Оголосіть схеми один раз, потім посилайтеся на них на контролер/метод.
$gen->addSecurityScheme('bearerAuth', [
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
]);
$gen->addSecurityScheme('apiKey', [
'type' => 'apiKey',
'in' => 'header',
'name' => 'X-API-Key',
]);
Потім застосуйте:
#[ApiSecurity(scheme: 'bearerAuth')]
final class UserController { /* застосовується до кожного методу */ }
#[ApiSecurity(scheme: 'apiKey')]
#[Post('/webhooks/incoming')]
public function webhook(Request $req): Response { /* лише цей метод */ }
Можна складати кілька схем (семантика АБО — достатньо будь-якої).
Віддача специфікації
Два підходи.
1. Статично — генерувати на етапі збірки
Найлегший варіант. Додайте CLI-команду, яка пише файл, і запускайте її під час деплою:
vendor/bin/lift make:openapi --output=public/openapi.json
Легко кешувати. Нульова вартість у рантаймі.
2. Динамічно — генерувати на запит
Якщо специфікація залежить від рантайм-конфігурації (різні схеми на орендаря, захищені маршрути), генеруйте її на льоту:
$app->get('/openapi.json', function () use ($gen) {
return Response::json($gen->generate())
->withHeader('Cache-Control', 'public, max-age=300');
});
Кешуйте на кілька хвилин — рефлексія не безкоштовна, але це всього кілька мс.
Рендеринг — Swagger UI / Redoc
Покладіть статичний HTML кудись під public/:
<!-- public/docs.html — Swagger UI через CDN -->
<!doctype html><html><head>
<title>API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
</head><body>
<div id="ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({ url: '/openapi.json', dom_id: '#ui' });
</script>
</body></html>
Та сама ідея з Redoc (один тег <script>).
Опрацьований приклад — повний контролер
use Lift\Attribute\{Get, Post, Group};
use Lift\OpenApi\Attribute\{ApiOperation, ApiParam, ApiResponse, ApiSecurity, ApiTag};
#[Group('/api/v1')]
#[ApiTag('Users', description: 'Account management')]
#[ApiSecurity(scheme: 'bearerAuth')]
final class UserController
{
public function __construct(private readonly UserRepository $repo) {}
#[Get('/users')]
#[ApiOperation(summary: 'List users')]
#[ApiParam('page', in: 'query', type: 'integer', description: 'Page number')]
#[ApiParam('per_page', in: 'query', type: 'integer', description: 'Items per page (max 100)')]
#[ApiResponse(200, schema: UserListResource::class)]
public function index(Request $req): Paginator { … }
#[Get('/users/{id:\d+}')]
#[ApiOperation(summary: 'Get one user', operationId: 'getUser')]
#[ApiResponse(200, schema: UserDTO::class)]
#[ApiResponse(404, description: 'User not found')]
public function show(Request $req): Response { … }
#[Post('/users')]
#[ApiOperation(summary: 'Create user')]
#[ApiParam('email', in: 'body', type: 'string', required: true)]
#[ApiParam('password', in: 'body', type: 'string', required: true)]
#[ApiResponse(201, schema: UserDTO::class)]
#[ApiResponse(422, description: 'Validation failed')]
public function store(Request $req): Response { … }
}
Генератор перетворює це на повну специфікацію /api/v1/users / /api/v1/users/{id} з параметрами, схемами відповідей і вимогою JWT на кожній операції — без жодного файлу YAML.
Обмеження
- Схеми тіла запиту поза простим
#[ApiParam(in: 'body')]не виражаються нативно. Для складних тіл зареєструйте DTO#[ApiSchema]й посилайтеся на нього з власногоrequestBody, доданого постобробкою згенерованого масиву. - Поліморфні / oneOf / discriminator схеми потребують форми
schema:із сирим рядком на#[ApiResponse]. - Генератор рефлексує лише публічні властивості. Моделям із гетерами потрібен DTO.
- Правила валідації (
'required|email|max:255') не перекладаються в обмеження OpenAPI автоматично. Дві системи живуть пліч-о-пліч; копіюйте обмеження в#[ApiParam], коли вам це важливо.
Це навмисні компроміси простоти — генератор чисто покриває 80% випадків і прибирається з дороги для решти.
Часті підводні камені
| Симптом | Причина | Виправлення |
|---|---|---|
| Метод відсутній у специфікації | На ньому немає атрибута маршруту (#[Get]/…) |
Генератор OpenAPI включає лише маршрути з атрибутами — імперативний $app->get('/x', ...) для нього невидимий. |
Шлях буквально показує {id:\d+} |
Стара версія генератора | Lift вирізає regex-обмеження зі шляхів OpenAPI автоматично — переконайтеся, що ви на поточному релізі. |
У схем усе типу string |
У DTO немає типізованих властивостей | Додайте типи (public int $id;) на DTO. |
| Swagger UI каже «no operations» | URL специфікації повернув 404 / невірний CORS | Зверніться до нього через curl — URL має бути доступний за CORS, якщо Swagger на іншому джерелі. |
| Специфікація регенерується на кожному запиті | Ви використовуєте динамічний маршрут + рефлексію | Кешуйте вивід JSON (Cache-Control або запис на диск під час збірки). |
| Безпека показується на операціях, яким вона не потрібна | #[ApiSecurity] був на рівні класу |
Перевизначте на метод через #[ApiSecurity(scheme: '')] — або реструктуруйте контролер. |
Шпаргалка
use Lift\OpenApi\Attribute\{ApiOperation, ApiParam, ApiResponse, ApiSecurity, ApiSchema, ApiTag};
#[ApiTag('Users')]
#[ApiSecurity(scheme: 'bearerAuth')]
final class UserController
{
#[Get('/users/{id:\d+}')]
#[ApiOperation(summary: '…', operationId: 'getUser')]
#[ApiParam('id', in: 'path', type: 'integer')]
#[ApiResponse(200, schema: UserDTO::class)]
#[ApiResponse(404, description: 'Not found')]
public function show(): Response { … }
}
$gen = new Generator(title: 'My API', version: '1.0.0', serverUrl: '/');
$gen->addController(UserController::class);
$gen->addSchema(UserDTO::class);
$gen->addSecurityScheme('bearerAuth', ['type' => 'http', 'scheme' => 'bearer']);
file_put_contents('public/openapi.json', $gen->toJson());