Современный API Reference в Symfony через Scalar

Работая с PHP, испытываешь постоянную необходимость поддерживать документацию в актуальном состоянии. В последнее время PHP проекты — это API‑first, когда бек на PHP, а фронт собирается отдельно.
Тут у нас два пути:
Spec‑first, то есть сначала готовится спецификация всех возможных маршрутов с входными/выходными параметрами, ошибками и пр. Часто это YAML‑файл OpenAPI.
OpenAPI файл генерируется на основе атрибутов и аннотаций в коде.
В первом случае мы делаем то, что прописано в спецификации и имеем возможность сравнить, верно ли у нас всё сделано. Во втором случае у нас всегда актуальная схема (после автоматического обновления).
Стандартом в Symfony считается NelmioApiDocBundle. Вплоть до 4 версии в качестве UI всё ещё отдаёт Swagger UI или Redoc. Если поискать, какие ещё есть варианты UI, находишь свежий и современный Scalar (В NelmioApiDocBundle 5+ версии заявлена поддержка), open‑source‑рендерером API Reference, но NelmioApiDocBundle генерирует OpenAPI из атрибутов — это эталонный представитель второго типа.
Осмотр конкурента Laravel, показывает, что там есть больше различных вариантов (Scribe, Scramble и официальная интеграция со Scalar).
Хотелось бы легкий, современный UI для отрисовки готового OpenAPI yaml файла для первого типа работы с документацией API.И, кажется, Scalar для этого отлично подходит, но вот интеграции из коробки у symfony со scalar нет. И чтобы подключить Scalar в Symfony, приходится копировать HTML‑страницу с <script>‑тегом из доков Scalar и настраивать конфигурацию руками.
В самом Scalar официальный список интеграций покрывает 30+ фреймворков ( Express, FastAPI, NestJS, Spring Boot, Laravel), но нет Symfony. На Packagist не было ни одного пакета, который подключал Scalar в Symfony.
В одном своём проекте у меня подход OpenAPI‑first, то есть сначала спецификация, потом реализация. Мощный NelmioApiDocBundle тянуть в проект не хотелось. Выбор пал на Scalar. Но одно плохо, нет интеграции. Можно было просто интегрировать своими силами в проекте, но понял, что можно оформить это в виде bundle и в дальнейшем переиспользовать в других проектах. За один вечер я написал первую версию Symfony‑бандла, тесты и CI‑матрицы. Протестировал это на своём проекте, после чего опубликован.
2. Разберемся подробнее, что такое Scalar
Scalar — open‑source API‑платформа для работы с OpenAPI‑документами.
Две части:
API Reference — интерактивный рендерер OpenAPI 3.x;
Загружает спеку на клиенте, со встроенным API‑клиентом (Возможность делать тестовые запросы),
есть сниппеты кода на разных языках (curl, PHP, Python, Go), разные темы оформления и разные схемы авторизации;API client — десктопный клиент уровня Postman, работает офлайн и читает те же OpenAPI‑файлы.
На данный момент в GitHub: около 16 тыс. звёзд, создан в 2023 году, насчитывает более 100 релизов, живой проект. Есть интеграция для Laravel (scalar/laravel, официальная, в org scalar) — это основа на что смотреть при реализации своего бандла. Что там есть? «Тонкий пакет», который отдаёт одну страницу со Scalar, указывая на любой OpenAPI‑документ.
3. Бандл: что он делает
alex-frolov/scalar-symfony рендерит Scalar API Reference в Symfony из любого OpenAPI‑документа.
Один маршрут, нет привязки к тому, каким образом сгенерировалась спецификация. Может быть статичный openapi.yaml, может swagger‑php, NelmioApiDocBundle или API Platform.
Для установки и настройки необходимо отредактировать два файла:
Подключаем бандл:
composer require alex-frolov/scalar-symfonyРедактируем настройки:
# config/packages/scalar_symfony.yaml
scalar_symfony:
url: '/openapi.yaml' # ваш OpenAPI-документ (обязательно)
path: '/scalar' # маршрут (по умолчанию: /scalar)
cdn: 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.65.1'
configuration:
theme: 'default'
metaData:
title: 'API Reference'
scalar_options: # любая опция Scalar, пробрасывается как есть
darkMode: false
layout: 'modern'
access_control:
mode: public # или 'attribute' + security-атрибут
# config/routes.yaml
scalar_symfony:
resource: '@ScalarSymfonyBundle/config/routes.php'
Всё, reference открывается по адресу /scalar.
Страница небольшой HTML с CDN‑скриптом и Scalar.createApiReference('#scalar-api-reference', {...}), где конфиг сериализуется XSS‑безопасно (JSON_HEX_TAG/APOS/AMP/QUOT).
4. Настоящий запрос, настоящий 201
Лучше всего показать это на живом примере. Бандл обслуживает API Reference платформы Tender (Symfony 8.5, highload‑аукционный API; спека — OpenAPI 3.1).
Через «Test Request» открываем эндпоинт POST /auth/register “Регистрация компании (создаётся компания pending + первый пользователь admin)” — заполнил JSON‑тело, нажал Send и получил:
HTTP/1.1 201 Created (794 ms)
{
"company_id": "0c7702c6-9667-4ea3-8caa-df4990522ee7",
"user_id": "86a40d70-5ef9-44fd-882b-8f703e10df7e",
"verification_status": "pending"
}
Реальный ответ с данными. При тестировании много вариантов, больше вариативность, чем у Swagger‑UI, работает как интерактивный клиент.

5. Контроль качества
Итоговое состояние:
Имеются 16 функциональных тестов с 46 проверками;
Проводиться статический анализ через PHPStan level max;
Контроль за стилем кода посредством PHP‑CS‑Fixer, правила
@Symfony;CI проверки PHP 8.2/8.3/8.5 c Symfony 6.4/7.2/7.4/8.0, включая опции
--prefer-lowest(5 job), no‑dev smoke‑тест иcomposer validate --strict;Ужесточение политик безопасности в конфиге, добавлены валидация на этапе компиляции, так режим
attributeбез Symfony Security роняетcache:clearс внятной ошибкой вместо HTTP 500 на первом запросе, пустойcdn, путь без ведущего/, пустойattribute, теперь всё блокируется на уровне конфига;Добавлена документация по безопасности, включая SRI (SHA-384 для указанного файла CDN), рекомендации по CSP/nonce, рецепт self‑hosting.
Два найденных артефакта.
PHPUnit 11.5 помечает тесты как risky, когда ErrorHandler Symfony идет после boot ядра, фикс восстановление exception‑handler в
tearDown().контейнер кэшируется по классу ядра и окружению, из‑за этого функциональные тесты с разными конфигами бандла должны использовать уникальный cache‑dir на каждый конфиг, иначе тесты запускаются из устаревшего контейнера с битым состоянием.
6. CI и проблемы
После пуша в репозиторий GitHub, запустился CI. Половина задач из GitHub Actions начала падать со случайной ошибкой:
Your github oauth token for github.com contains invalid charactersКак выяснилось, причина была в setup-php, он пишет Actions‑токен GITHUB_TOKEN с префиксом ghs_ в глобальный auth.json composer, а Composer версии 2.8 принимает только ghp_/gho_/github_pat_. Падение тасков было плавающим, зависело от того, заставил ли rate‑limit реально использовать токен или нет, поэтому часть тасков проходила. Даже composer config --unset падал с той же ошибкой.
Пришлось делать фикс, удалять auth.json на виртуальном раннере перед проверкой composer validate --no-check-publish, а токен оставлять для установки зависимостей, где он защищает от rate‑limit.
7. Roadmap и предложение
Я открыл предложение на старнице Scalar:
Discussion #9920 — “Proposal: official Symfony integration (scalar/symfony)”
Суть: принять бандл в org scalar как scalar/symfony, повторив ровно путь scalar/laravel — перенос или форк, добавить трекинг _integration: symfony, занести Symfony в официальные интеграции. Бандл опубликован, протестирован, работает в проде, усилия на принятие почти нулевые, а разработчики Symfony получают то, что у Laravel уже есть.
Если вы Symfony‑разработчик и хотите этого, то ставьте реакции и комментируйте обсуждение. Внимание мейнтейнеров следует за сигналом сообщества.
8. Как начать использовать?
composer require alex-frolov/scalar-symfonyУкажите
scalar_symfony.urlна любой OpenAPI‑документ (или сгенерируйте через NelmioApiDocBundle / swagger‑php)Импортируйте маршруты, откройте
/scalar
В README описано всё подробно.
9. Итог
У Scalar не было официальной интеграции с Symfony, была потребность, был сделан бандл.
Бандл v0.1.0,
alex-frolov/scalar-symfony.Поддерживаемые версии PHP >= 8.2, Symfony 6.4 / 7.2+ / 8.x.
Использовано у себя на платформе Tender Platform.
Код бандла: github.com/alex‑frolov/scalar‑symfony
Предложение: scalar/scalar#discussion-9920
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.