![OpenAPI без #[OA\...]: как я сделал генератор документации для Symfony](/_next/image?url=https%3A%2F%2Fapi.ktohto.ru%2Fapi%2Fv1%2Fmedia%2F731b374e-0c67-43dc-9f26-5b26a113702a.png&w=3840&q=75)
OpenAPI без #[OA\...]: как я сделал генератор документации для Symfony
Статья была полезной?
В прошлой статье я уже делился опытом о том, как однажды устроился на работу и столкнулся с задачей вручную синхронизировать OpenAPI, оформленный в комментариях к коду, с самим кодом в нескольких сервисах.
Тогда это казалось началом анекдота, но мне не было до смеха.
С тех пор я сменил место работы. И, как будто Вселенная попыталась проверить моё чувство юмора, я снова сталкиваюсь с API, где контракт находится рядом с кодом в ручных #[OA\...] атрибутах.
Это примечательный момент. Это не история о конкретной компании или неудачном проекте; я вижу этот подход на разных уровнях.
Symfony уже понимает маршруты, контроллеры знают входные параметры, а DTO описывает запрос. Однако поверх этого часто создаётся ещё один слой:
#[OA\Post(
path: '/v1/completions',
requestBody: new OA\RequestBody(...),
responses: [
new OA\Response(
response: 200,
description: 'Completion result',
content: new OA\JsonContent(...),
),
],
)]
#[Route('/v1/completions', methods: ['POST'])]
public function __invoke(CreateCompletionRequest $request): JsonResponse
{
// ...
}Я не рассматриваю OpenAPI как проблему. Наоборот, OpenAPI — это полезный инструмент, а Swagger UI также приносит свою пользу. Контракт API важен.
Сложность заключается в том, что мы часто дублируем то, что уже присутствует в коде.
Изменил DTO — не забудь обновить OpenAPI. Изменился response — не забудь обновить OpenAPI. Если не обновил — в лучшем случае ожидай бага.
Я занимаюсь этим вопросом уже несколько лет и придерживаюсь мнения, что, в большинстве случаев, документация API должна генерироваться из кода, а не существовать рядом с ним как отдельный ручной артефакт.
Именно по этой причине я разработал sunrise-studio/symfony-openapi.
Я хотел, чтобы стандартный Symfony контроллер выглядел примерно так:
declare(strict_types=1);
namespace App\Http\Controller;
use App\Http\Request\CreateCompletionRequest;
use App\Http\View\CompletionView;
use App\Service\CompletionService;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions', methods: ['POST'])]
final readonly class CreateCompletionController
{
public function __construct(
private CompletionService $completionService,
) {
}
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): CompletionView {
return new CompletionView(
text: $this->completionService->complete($request->prompt),
);
}
}Request DTO:
declare(strict_types=1);
namespace App\Http\Request;
final readonly class CreateCompletionRequest
{
public function __construct(
public string $prompt,
) {
}
}View object для ответа:
declare(strict_types=1);
namespace App\Http\View;
final readonly class CompletionView
{
public function __construct(
public string $text,
) {
}
}Маршрут описывает путь и метод.#[MapRequestPayload] указывает, откуда брать входные данные.
Request DTO описывает тело запроса.
Return type контроллера описывает ответ.
View object представляет собой конечный результат.
По задумке, этого уже должно быть достаточно для генерации OpenAPI-документа.
Правда, иногда возрастают сложности. Но для большинства методов API этого действительно хватает.
Во времена AI-ажиотажа можно, конечно, рассмотреть проблему следующим образом:
declare(strict_types=1);
namespace App\Command;
use App\Ai\ArtificialIntelligenceInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('app:generate-openapi')]
final readonly class GenerateOpenApiCommand extends Command
{
public function __construct(
private ArtificialIntelligenceInterface $ai,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$document = $this->ai->complete(
<<<'PROMPT'
Analyze all Symfony controllers in src/Http/Controller.
Generate a valid OpenAPI 3.1 JSON document based on routes,
request DTOs and response types.
Return only raw JSON without Markdown, comments or explanations.
PROMPT,
);
file_put_contents(__DIR__ . '/../../var/openapi.json', $document);
return Command::SUCCESS;
}
}Я не серьёзно.
Очень надеюсь, что никто не принимает эту шутку всерьёз.
Не мог не поделиться этим примером, так как он показался мне забавным, и надеюсь, что вам тоже!
И дело не только в том, что AI может ошибаться. Он обязательно сделает это. Вопрос заключается в том, когда и насколько заметно это будет.

Я, заставляю свой мозг работать без ИИ
Я, заставляю свой мозг работать без ИИ
sunrise-studio/symfony-openapi генерирует OpenAPI-документ на основе существующего кода в вашем Symfony-приложении:
Symfony маршруты;
сигнатуры контроллеров;
атрибуты Symfony HttpKernel;
типизированные DTO и View классы;
опции маршрутов;
небольшие OpenAPI атрибуты для случаев, когда информации недостаточно.
Основная цель проста: обычные endpoints не должны требовать больших блоков #[OA\...].
Если endpoint простой, он должен документироваться почти автоматически. Если endpoint требует особой обработки, можно вмешаться вручную, добавив детали.
Я постарался подробно описать дополнительные функции в README. Там есть информация о установке, конфигурации, опциях маршрутов, сопоставлении запросов, ответах, обработки ошибок, ручных фрагментах OpenAPI, резоляторах схем и точках расширения.
Как минимум, это будет полезно не только вам, но и AI, которому вы потом об этом расскажете и попросите включить пакет в проект.
Установка:
composer require sunrise-studio/symfony-openapiПодключите пакет:
// config/bundles.php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Sunrise\Symfony\OpenApi\OpenApiBundle::class => ['all' => true],
];Импортируйте маршруты пакета:
# config/routes.yaml
openapi:
resource: '@OpenApiBundle/config/routes.php'После этого у вас будут доступны два маршрута:
GET /docs
GET /docs/openapi.json/docs открывает Swagger UI./docs/openapi.json возвращает OpenAPI JSON документ.
Базовая конфигурация может выглядеть следующим образом:
# config/packages/openapi.yaml
parameters:
openapi.initial_document:
openapi: 3.1.1
info:
title: API
version: 1.0.0Сгенерировать документ в файл можно с помощью следующей команды:
php bin/console openapi:build-documentПо умолчанию команда сохраняет документ в файл, указанный в openapi.document_filename.
Если вам нужен другой путь для Swagger UI, вы можете создать маршрут вручную:
# config/routes.yaml
swagger_ui:
path: /swagger.html
controller: Sunrise\Symfony\OpenApi\Controller\SwaggerController
methods: [GET]
options:
api: falseЕсли вы изменяете путь к OpenAPI-документу, не забудьте обновить и маршрут, и openapi.document_uri, чтобы Swagger UI мог загрузить правильный документ:
# config/routes.yaml
openapi_document:
path: /openapi.json
controller: Sunrise\Symfony\OpenApi\Controller\DocumentController
methods: [GET]
options:
api: false# config/packages/openapi.yaml
parameters:
openapi.document_uri: /openapi.jsonВернёмся к примеру с completions.
declare(strict_types=1);
namespace App\Http\Controller;
use App\Http\Request\CreateCompletionRequest;
use App\Http\View\CompletionView;
use App\Service\CompletionService;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions', methods: ['POST'])]
final readonly class CreateCompletionController
{
public function __construct(
private CompletionService $completionService,
) {
}
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): CompletionView {
return new CompletionView(
text: $this->completionService->complete($request->prompt),
);
}
}Здесь не используются:
#[OA\Post(...)]
#[OA\RequestBody(...)]
#[OA\Response(...)]
#[OA\JsonContent(...)]Суть в том, что мне не нужно повторно описывать то, что уже есть в коде.
Маршрут уже знает путь и метод.CreateCompletionRequest содержит информацию о теле запроса.CompletionView описывает тело ответа.
Return type контроллера уже говорит, что endpoint возвращает CompletionView.
Иногда необходимо дополнить информацию о тегах, резюме, описании или статус-коде.
Я не хотел превращать это обратно в объёмный блок OpenAPI, поэтому метаданные можно разместить ближе к маршруту:
declare(strict_types=1);
namespace App\Http\Controller;
use App\Http\Request\CreateCompletionRequest;
use App\Http\View\CompletionView;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
#[Route(
'/v1/completions',
methods: ['POST'],
options: [
'tags' => ['Completions'],
'summary' => 'Creates completion',
'description' => 'Creates text completion for the given prompt.',
'response_code' => 201,
],
)]
final readonly class CreateCompletionController
{
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): CompletionView {
// ...
}
}Это всё ещё выглядит как описание маршрута и поведения endpoint-а, а не как отдельный OpenAPI-документ внутри PHP-атрибута.
Поддерживаются, такие параметры, как:
tag, tags;
summary;
description;
deprecated;
api;
response_code;
response_format, response_formats.
Если вашему проекту не подходит хранение этих атрибутов в опциях маршрута, есть возможность заменить RouteMetadataResolverInterface.
Пакет понимает атрибуты Symfony, которые описывают данные запроса.
Request body обозначается с помощью #[MapRequestPayload]:
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions', methods: ['POST'])]
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): CompletionView {
// ...
}Path переменные считываются из маршрута и сигнатуры метода:
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions/{id}', methods: ['GET'])]
public function __invoke(string $id): CompletionView
{
// ...
}Запросный объект можно описать через #[MapQueryString]:
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions', methods: ['GET'])]
public function __invoke(
#[MapQueryString] CompletionListQuery $query,
): CompletionListView {
// ...
}Обычные скалярные параметры для запроса можно описывать с помощью #[MapQueryParameter].
Я не могу и не хочу навязывать всем одинаковую архитектуру. У проектов бывают разные требования, включая легаси, соглашения, странные интеграции и множество других факторов, которые часто возникают в процессе разработки после фразы «это временно».
Однако, в нескольких проектах я уже отказался от ручного возврата JsonResponse/Response из большинства API-контроллеров.
На мой взгляд, для API это более элегантно:
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): CompletionView {
return new CompletionView(
text: $this->completionService->complete($request->prompt),
);
}Контроллер возвращает результат операции,
HTTP-слой отвечает за сериализацию,
OpenAPI-генератор видит return type и может построить схему.
Это не строгое соблюдение DDD; скорее, это нормальная граница между приложенческим кодом и HTTP-представлением. DTO описывает входные данные, View object описывает выходные данные, а контроллер перестаёт быть местом, где собираются массивы JSON, статус-коды и документация вручную.
У меня есть проект с более чем 100 API методами, и лишь в нескольких случаях пришлось вернуть Response напрямую. Такие ситуации бывают, и это нормально.
Для таких случаев предусмотрено ручное описание операции через #[Operation].
Если endpoint возвращает Response, пакет не может автоматически понимать тело ответа. И это обоснованно: если вы возвращаете низкоуровневый Response, значит вы самостоятельно контролируете ответ.
Однако даже в этом случае нет необходимости возвращаться к объёмным #[OA\...] атрибутам.
Можно использовать #[Operation] и Type:
declare(strict_types=1);namespace App\Http\Controller;
use App\Http\Request\CreateCompletionRequest;
use App\Http\View\CompletionView;
use Sunrise\Symfony\OpenApi\Annotation\Operation;
use Sunrise\Symfony\OpenApi\Type;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions/stream', methods: ['POST'])]
final readonly class StreamCompletionController
{
#[Operation([
'responses' => [
200 => [
'description' => 'Completion stream.',
'content' => [
'application/json' => [
'schema' => new Type(CompletionView::class),
,],
,],
,],
])]
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): Response {
// corner case
}
}
Type удобен тем, что можно сослаться на PHP-класс, а пакет сам превратит его в OpenAPI schema.
Ручной режим есть, но он, как видно на примере выше, точечный.
Новые возможности Symfony 8.1: сериализация результатов контроллераВ Symfony 8.1 появился нативный #[Serialize], который позволяет вернуть из контроллера объект или массив, а Symfony сам сериализует результат в Response.
Это очень приятный бонус.
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Attribute\Serialize;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/v1/completions', methods: ['POST'])]
#[Serialize(code: 201)]
public function __invoke(
#[MapRequestPayload] CreateCompletionRequest $request,
): CompletionView {
// ...
}Если #[Serialize] есть, пакет может учитывать его code, а schema всё равно берётся из PHP return type.
Но я бы не стал обновлять Symfony только ради документации. Если проект ниже 8.1, runtime-сериализация результата контроллера делается небольшим listener-ом.
Например, JSON-only вариант может выглядеть так:
declare(strict_types=1);
namespace App\Http\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\SerializerInterface;
#[AsEventListener(event: KernelEvents::VIEW)]
final readonly class JsonControllerResultListener
{
public function __construct(
private SerializerInterface $serializer,
private RouterInterface $router,
) {
}
public function __invoke(ViewEvent $event): void
{
$result = $event->getControllerResult();
if ($result === null) {
$event->setResponse(new Response(status: Response::HTTP_NO_CONTENT));
return;
}
$event->setResponse(new JsonResponse(
data: $this->serializer->serialize($result, 'json'),
status: $this->resolveResponseCode($event),
json: true,
));
}
private function resolveResponseCode(ViewEvent $event): int
{
$routeName = $event->getRequest()->attributes->get('_route');
if (!is_string($routeName)) {
return Response::HTTP_OK;
}
$route = $this->router->getRouteCollection()->get($routeName);
if (!$route instanceof Route) {
return Response::HTTP_OK;
}
return $route->getOption('response_code') ?? Response::HTTP_OK;
}
}Такой listener не должен поставляться с пакетом.
Это выходит за рамки его ответственности. У каждого проекта могут быть свои правила: поддержка заголовка Accept, разные форматы ответа, serializer groups, логирование, нестандартные заголовки, динамическая сериализация и так далее.
Пакет отвечает за генерацию OpenAPI-документа. Runtime-поведение приложения должно оставаться под контролем самого приложения.
Возможные изменения в проекте для оптимизации APIПакет можно поставить и попробовать достаточно быстро.
Но если хочется получить от него максимум пользы, возможно, придётся привести в порядок сам API-слой.
1. Минимизация зависимости от HttpFoundation Request/Response в контроллерахЕсли контроллеры везде принимают Request и возвращают JsonResponse, генератору сложнее понять публичный контракт.
Я бы рекомендовал для обычных API methods использовать:
request DTO для входа;
View objects для выхода;
typed properties;
явные return types;
минимальное количество ручных OpenAPI-фрагментов.
Не потому что «так надо по книжке», а потому что так проще читать код, проще рефакторить и проще генерировать документацию.
2. Единый формат обработки ошибокОтдельно стоит пересмотреть обработку ошибок.
Очень неудобно, когда один endpoint возвращает:
{"message": "Validation failed"}другой:
{"error": "Bad request"}а третий:
{"success": false, "data": null, "exception": "..."}Тут никакой генератор не спасёт. Он может описать контракт, но если контракт размазан по проекту в виде разных случайных форматов, документировать особо нечего.
В документации есть отдельный раздел про документирование ошибок. Я бы рекомендовал привести ошибки к единому виду и уже его документировать.
Например, view для ошибки может выглядеть так:
declare(strict_types=1);
namespace App\Http\View;
use Symfony\Component\Validator\ConstraintViolationListInterface;
final readonly class ErrorResponseView
{
public function __construct(
public string $message,
/** @var array */
public array $errors = [],
) {
}
public static function fromViolationList(
ConstraintViolationListInterface $violations,
?string $message = null,
): self {
return new self(
message: $message ?: 'Validation failed',
errors: array_map(ErrorView::fromViolation(...), [...$violations]),
);
}
}declare(strict_types=1);
namespace App\Http\View;
use Symfony\Component\Validator\ConstraintViolationInterface;
final readonly class ErrorView
{
public function __construct(
public string $key,
public string $message,
) {
}
public static function fromViolation(ConstraintViolationInterface $violation): self
{
return new self(
key: $violation->getPropertyPath(),
message: (string) $violation->getMessage(),
);
}
}А ExceptionSubscriber может приводить исключения к этой форме:
declare(strict_types=1);
namespace App\Http\Subscriber;
use App\Http\View\ErrorResponseView;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
final readonly class ExceptionSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger,
private KernelInterface $kernel,
) {
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [KernelEvents::EXCEPTION => 'onKernelException'];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if ($exception instanceof HttpExceptionInterface) {
$event->setResponse($this->buildHttpErrorResponse($exception));
return;
}
$this->logger->error($exception->getMessage(), [
'exception' => $exception,
]);
$event->setResponse($this->buildFatalErrorResponse($exception));
}
private function buildErrorResponse(string $message, int $status): JsonResponse
{
$message = $message ?: Response::$statusTexts[$status] ?? (string) $status;
return new JsonResponse(new ErrorResponseView($message), $status);
}
private function buildHttpErrorResponse(HttpExceptionInterface $exception): Response
{
$previous = $exception->getPrevious();
$response = null;
if ($previous instanceof ValidationFailedException) {
$response = $this->buildValidationErrorResponse(
$previous->getViolations(),
$exception->getMessage(),
$exception->getStatusCode(),
);
}
$response ??= $this->buildErrorResponse(
$exception->getMessage(),
$exception->getStatusCode(),
);
$response->headers->add($exception->getHeaders());
return $response;
}
private function buildValidationErrorResponse(
ConstraintViolationListInterface $violations,
?string $message = null,
?int $status = null,
): JsonResponse {
return new JsonResponse(
ErrorResponseView::fromViolationList($violations, $message),
$status ?? Response::HTTP_BAD_REQUEST,
);
}
private function buildFatalErrorResponse(\Throwable $exception): Response
{
$message = $this->kernel->isDebug()
? (string) $exception
: 'Something went wrong';
return $this->buildErrorResponse(
$message,
Response::HTTP_INTERNAL_SERVER_ERROR,
);
}
}После этого общий error response можно описать через openapi.initial_operation:
# config/packages/openapi.yaml
parameters:
openapi.initial_operation:
responses:
default:
description: The operation was unsuccessful.
content:
application/json:
schema: 'App\Http\View\ErrorResponseView'Или точечно через #[Operation].
3. Единый формат для датЯ бы отдельно навёл порядок с датами.
Например:
# config/services.yaml
parameters:
app_output_timestamp_format: 'Y-m-d\TH:i:s.u'# config/packages/serializer.yaml
framework:
serializer:
enabled: true
default_context:
datetime_format: '%app_output_timestamp_format%'# config/packages/openapi.yaml
parameters:
openapi.default_timestamp_format: '%app_output_timestamp_format%'Так runtime-сериализация и OpenAPI examples хотя бы смотрят в одну сторону.
Обработка массивов и item typesОтдельная боль PHP — массивы.
Сам тип array почти ничего не говорит о публичном контракте. Это может быть список строк, список объектов, ассоциативный массив, карта ошибок, что угодно.
Для таких случаев пакет умеет читать item type из PHPDoc:
declare(strict_types=1);
namespace App\Http\View;
final readonly class CompletionListView
{
public function __construct(
/** @var CompletionView[] */
public array $items,
) {
}
}Поддерживаются разные формы описания item type, а если нужно явное переопределение, есть #[ItemType].
Я не хочу превращать статью в пересказ README, поэтому подробности лучше смотреть в документации.
Ограничения и возможности пакетаВажно честно сказать: пакет не пытается угадать вообще всё.
DTO и View объекты описываются по типизированным свойствам. Пакет не обязан читать всю runtime-магию Symfony Serializer: groups, getters, setters, SerializedName, name converters или camelCase/snake_case conversion rules.
Я считаю это нормальным компромиссом.
Для публичного API явные DTO и View объекты часто проще, надёжнее и лучше переживают рефакторинг. Если нужна другая внешняя форма, можно сделать отдельный View объект и замапить в него сущность.
Если вашей команде нужна first-class поддержка Symfony Serializer metadata, это уже отдельная задача и отдельная стратегия, а не то, что стоит неявно смешивать с базовой генерацией схем.
Точки расширения для кастомизацииПакет собран из заменяемых сервисов для проектов со своими соглашениями.
Можно заменить или расширить:
RouteMetadataResolverInterface;
ResponseMetadataResolverInterface;
OpenApiOperationEnricherInterface;
OpenApiPhpTypeSchemaResolverInterface;
OpenApiPathBuilderInterface.
Например, если у проекта свои правила для status codes или response formats, можно заменить ResponseMetadataResolverInterface.
Если нужен особый PHP type, можно реализовать OpenApiPhpTypeSchemaResolverInterface и зарегистрировать resolver.
Идея в том, чтобы не форкать пакет ради каждого проектного правила.
Итоги использования пакетаЯ хотел получить инструмент, который в обычном случае работает по принципу:
установил, подключил, описал нормальные DTO/View objects, получил OpenAPI.
Почти «установил и забыл».
Разумеется, в реальном проекте всё равно есть нюансы: ошибки, даты, runtime-сериализация, нестандартные ответы, внутренние соглашения. Но это уже нормальная инженерная настройка, а не ручное переписывание одного и того же контракта в двух местах.
И, возможно, главный эффект даже не в Swagger UI.
Пакет может подтолкнуть чей-то, а может быть и ваш, проект к более чистому API:
меньше ручной сборки JsonResponse;
меньше дублирования;
больше typed DTO;
больше явных View objects;
понятнее ошибки;
прозрачнее даты;
проще рефакторинг.
Я не говорю, что у всех в проектах бардак. Я понятия не имею, что у вас в проектах. Но на моём опыте последних лет вокруг API часто не очень аккуратно, и я рад, что могу хотя бы немного повлиять на это.
Документация пакета и полезные ресурсыДокументация пакета лежит здесь:
README-ru.md
Я постарался описать не только «happy path», но и реальные пользовательские сценарии:
установка;
маршруты документации;
генерация документа;
route options;
Symfony attributes;
responses;
ручные OpenAPI-фрагменты;
ошибки;
type schema resolvers;
object schemas;
extension points.
Возможно, вы прочитаете её сами.
Возможно, скормите её ИИ и попросите помочь подключить пакет.
В любом случае идея та же: меньше ручного описания OpenAPI, больше нормального кода, из которого контракт можно вывести автоматически.
Главное, чтобы ИИ не начал сам придумывать OpenAPI вместо того, чтобы использовать уже существующую архитектуру.
Потенциал для дальнейшего развития APIЕсли вам близка сама идея API, где документация является естественным продолжением кода, а не отдельным этапом работы, можно посмотреть и на sunrise/http-router.
Про него я уже писал в прошлой статье.
Symfony bundle появился не вместо этого маршрутизатора, а как продолжение той же идеи для Symfony-проектов. Если у вас Symfony — можно попробовать sunrise-studio/symfony-openapi. Если хочется попробовать другой маршрутизатор и чуть другую архитектуру API — можно посмотреть в сторону Sunrise Router.
Заключение: важность OpenAPI и автоматизации документацииOpenAPI полезен. Swagger UI полезен. Контракт API полезен.
Но если контракт приходится постоянно синхронизировать руками с кодом, рано или поздно он начнёт расходиться с реальностью. Это неизбежно.
Я считаю, что в Symfony уже достаточно информации, чтобы большую часть OpenAPI-документации генерировать автоматически.
Маршруты, сигнатуры, DTO, View objects и небольшое количество metadata — этого обычно хватает.
Именно так я и хотел сделать.
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…