API Platform на Symfony: когда брать, а когда обойтись
API Platform обещает REST и GraphQL из коробки поверх Doctrine-сущностей. Разбираем, где это экономит недели, а где превращает простой эндпоинт в борьбу с абстракциями.

Вы завели сущность Product, повесили пару атрибутов — и получили работающий REST API с пагинацией, фильтрами, документацией OpenAPI и GraphQL. Так продают API Platform, и на демо это правда работает. Проблемы начинаются, когда бизнес-логика перестаёт совпадать со схемой базы: приходит требование «на создание заказа проверь остаток на складе, спиши бонусы и отправь письмо», и удобная магия State Processor'ов оборачивается тремя днями чтения исходников фреймворка.
Здесь сравниваем три способа отдавать API в проекте на Symfony 7.x: API Platform 4.x, ручные контроллеры с сериализацией через Serializer-компонент и гибрид, где часть ресурсов на API Platform, часть — руками. Цель — понять, за что вы платите в каждом варианте, чтобы не выбирать по маркетинговой странице.
Три варианта на одной задаче
Возьмём типичный интернет-магазин: сущности Product, Order, Customer. Часть ресурсов — чистое CRUD-чтение справочников, часть — операции с логикой (оформление заказа, смена статуса). На этой смеси и видно разницу.
Вариант 1. API Platform целиком
Вы описываете ресурс атрибутом прямо на Doctrine-сущности. Операции, сериализационные группы, фильтры — всё декларативно.
// src/Entity/Product.php — API Platform 4.x, Symfony 7.x
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Get;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
],
normalizationContext: ['groups' => ['product:read']],
)]
class Product
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
#[Groups(['product:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['product:read'])]
private string $name;
// геттеры/сеттеры опущены
}После этого GET /api/products отдаёт коллекцию с пагинацией и Hydra-документацией без единой строки контроллера. Для справочников это честная экономия.
Бизнес-логику вешают на State Processor — сервис, который перехватывает операцию записи. Здесь и начинается порог входа: чтобы к стандартному сохранению добавить проверку склада, вы оборачиваете штатный процессор своим.
// src/State/OrderProcessor.php
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
final class OrderProcessor implements ProcessorInterface
{
public function __construct(
// штатный процессор Doctrine, чтобы не терять сохранение
private ProcessorInterface $persistProcessor,
private StockChecker $stockChecker,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
// своя логика до сохранения
$this->stockChecker->assertAvailable($data);
// делегируем сохранение штатному процессору
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}Работает, но чтобы дойти до этого шаблона, нужно понять цепочку State Provider → State Processor и то, как её конфигурировать через #[ApiResource]. Точные имена сервисов для декорирования зависят от версии — сверяйте по разделу State в документации API Platform для вашей 4.x, они менялись между мажорными релизами.
Вариант 2. Ручные контроллеры
Обычный контроллер Symfony, сериализация через компонент Serializer, валидация через Validator. Никакой магии — и никакого слоя, который надо изучать.
// src/Controller/ProductController.php — Symfony 7.x
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
class ProductController
{
#[Route('/api/products', methods: ['GET'])]
public function list(ProductRepository $repo, SerializerInterface $serializer): JsonResponse
{
$products = $repo->findAll();
$json = $serializer->serialize($products, 'json', ['groups' => ['product:read']]);
return new JsonResponse($json, 200, [], true);
}
}Всё под контролем: вы видите каждый запрос, каждый ответ. Цена — то, что API Platform давал бесплатно, вы пишете руками: пагинация, фильтры, документация OpenAPI, обработка ошибок в едином формате. На пяти эндпоинтах это ерунда, на пятидесяти — заметный объём однотипного кода.
Вариант 3. Гибрид
Справочники и типовое CRUD — на API Platform. Сложные операции с логикой — отдельными ручными контроллерами под своими маршрутами. API Platform не запрещает соседство: вы просто не описываете ресурсом то, что удобнее написать руками.
Минус в том, что документация и формат ответов расходятся: коллекция от API Platform приходит в Hydra/JSON-LD, а ваш ручной эндпоинт — в плоском JSON. Клиенту придётся разбирать два формата, либо вы приводите ручные ответы к общему виду вручную.
Таблица: за что платите
| Критерий | API Platform | Ручные контроллеры | Гибрид |
|---|---|---|---|
| Скорость на CRUD | Минуты на ресурс | Час на ресурс | Минуты для справочников |
| OpenAPI/документация | Из коробки | Настраивать отдельно | Частичная, расходится |
| GraphQL | Включается флагом | Отдельная библиотека | Только для ресурсов AP |
| Сложная логика записи | Через State Processor, порог входа | Обычный сервис в контроллере | Логику — в ручную часть |
| Порог входа команды | Высокий: абстракции AP | Знания Symfony хватает | Средний |
| Единый формат ответов | Да (Hydra/JSON-LD) | Задаёте сами | Два формата рядом |
API Platform экономит время ровно до тех пор, пока ваша модель данных совпадает с моделью API. Как только появляется третий слой между сущностью и ответом, экономия превращается в изучение чужих абстракций.
Когда что выбирать
- Берите API Platform, если API — это в основном чтение и запись справочников, вам нужны GraphQL и готовая OpenAPI-документация, а команда готова один раз выучить State Provider/Processor. Админки, каталоги, дашборды с типовыми ресурсами — его территория.
- Берите ручные контроллеры, если эндпоинтов немного (условно до 10–15), почти каждый несёт нетривиальную логику, а формат ответа диктует внешний клиент, под который магия только мешает. Мобильный бэкенд с горстью операций — сюда.
- Берите гибрид, если справочников много, а операций с логикой мало и они изолированы. Только заранее решите, приводите ли ручные ответы к формату API Platform или сознательно живёте с двумя форматами.
Что ломается на практике
Три частые боли, из-за которых команды жалеют о выборе API Platform:
- Сериализационные группы разрастаются. Один и тот же
Orderв списке, в детальном ответе и во вложении внутриCustomerтребует разных наборов полей. Группы начинают ветвиться (order:read,order:read:list,order:read:nested), и держать их в голове тяжело. - N+1 на связях. API Platform по умолчанию отдаёт связанные ресурсы, и без явной настройки выборки (eager fetch или join в кастомном State Provider) коллекция из 50 заказов выльется в сотни запросов. Ловится профилировщиком Symfony по счётчику Doctrine-запросов на странице.
- Кастомная операция вне CRUD. Условный
POST /orders/{id}/refundне ложится в стандартные Get/Post/Patch. Его делают отдельной операцией со своим процессором, и это ощутимо больше кода, чем обычный экшен в контроллере.
Ручные контроллеры этих проблем не создают, но перекладывают на вас всё остальное: единый формат ошибок, версионирование, документацию. Выбор — это не «удобно против неудобно», а «где вы хотите тратить время».
Что сделать дальше
Не решайте на бумаге. Возьмите две-три реальные сущности своего проекта — обязательно включая ту, у которой логика сложнее CRUD, — и соберите на них прототип на API Platform за пару часов. Смотрите на счётчик SQL-запросов в профилировщике на списочных эндпоинтах и на то, сколько кода уходит на одну нестандартную операцию. Если прототип нестандартной операции занял больше, чем занял бы обычный контроллер, — это ваш сигнал в сторону гибрида. Дальше читайте раздел State в документации вашей версии 4.x: именно там лежит граница между «магия работает» и «магию приходится переопределять».
Дачник и агроном: Выращивание, борьба с вредителями, лунный календарь.
Спросить за 8 ₽Источники: API Platform — Official Documentation, Symfony — The Serializer Component
Частые вопросы
У меня API Platform 3.x, а не 4.x — примеры подойдут?
Можно ли отдавать чистый JSON вместо JSON-LD/Hydra?
Что делать, если после подключения API Platform запросы к базе взлетели?
Грозит ли переход на API Platform переписыванием всей аутентификации?
А если команда не знает API Platform — сколько закладывать на разгон?
Стоит ли включать GraphQL, если он нужен паре эндпоинтов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.