Lift v1.3.0
Документация
На этой странице

Генератор 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());

Отладочная панель →