SymfonyMiddleCoding

Как реализовать API Platform в проекте Symfony?

API Platform превращает PHP-класс с атрибутом #[ApiResource] в полноценный REST/GraphQL API: маршруты, сериализацию, валидацию и OpenAPI-документацию фреймворк генерирует автоматически.

Что делает API Platform

API Platform — это слой поверх Symfony, который берёт Doctrine-сущность (или любой PHP-класс) и автоматически генерирует для неё набор HTTP-операций: GET /products, GET /products/{id}, POST /products, PUT, PATCH, DELETE. Маршруты, сериализация через Symfony Serializer, валидация через Validator и OpenAPI-документ появляются без написания контроллеров вручную.

Минимальная настройка

Установка через Composer (внутри контейнера):

composer require api

Сущность с атрибутом #[ApiResource]:

<?php
// src/Entity/Product.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(),
        new Put(),
        new Delete(),
    ]
)]
#[ORM\Entity]
class Product
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Assert\Length(max: 255)]
    public string $name = '';

    #[ORM\Column]
    #[Assert\PositiveOrZero]
    public float $price = 0.0;

    public function getId(): ?int { return $this->id; }
}

После миграции (bin/console doctrine:migrations:migrate) сразу доступны все шесть эндпоинтов, а по адресу /api открывается Swagger UI.

Кастомная операция с StateProcessor

Когда стандартной персистентности через Doctrine недостаточно — например, нужно отправить email после создания заказа — подключают кастомный процессор:

<?php
// src/State/OrderProcessor.php
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Order;
use App\Service\Mailer;

final class OrderProcessor implements ProcessorInterface
{
    public function __construct(
        private ProcessorInterface $persistProcessor, // декорируем стандартный
        private Mailer $mailer,
    ) {}

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Order
    {
        /** @var Order $order */
        $order = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
        $this->mailer->sendOrderConfirmation($order);
        return $order;
    }
}
#[ApiResource(
    operations: [
        new Post(processor: OrderProcessor::class),
    ]
)]
class Order { /* ... */ }

Фильтрация и пагинация

use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Metadata\ApiFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])]
#[ApiFilter(RangeFilter::class, properties: ['price'])]
class Product { /* ... */ }

Теперь работают запросы вида GET /products?name=phone&price[gte]=100. Пагинация включена по умолчанию (30 элементов на страницу), настраивается в config/packages/api_platform.yaml.

Подводные камни

  • Circular reference при сериализации. Если сущности ссылаются друг на друга (Order → User → Order), Serializer уходит в бесконечную рекурсию. Решение — группы сериализации (#[Groups(['order:read'])]) на каждом свойстве.
  • N+1 запросы в коллекциях. По умолчанию Doctrine подгружает связанные сущности по одной. Нужен fetch: EAGER или кастомный StateProvider с JOIN FETCH.
  • Конфликт маршрутов с обычными контроллерами. API Platform регистрирует маршруты с префиксом /api; если у вас уже есть контроллер на этом пути, возникнет конфликт. Проверяйте через bin/console debug:router.
  • Валидация срабатывает до процессора. Symfony Validator запускается автоматически перед process(). Если добавить кастомный Constraint, который делает запрос в БД, он выполнится на каждый POST — учитывайте производительность.
  • Версионирование API. API Platform не поддерживает версионирование «из коробки». Придётся либо использовать разные префиксы (/api/v1, /api/v2), либо content negotiation через custom operation.
  • GraphQL и Mercure требуют отдельных пакетов. composer require webonyx/graphql-php и composer require mercure — без них соответствующие разделы документации не работают, хотя атрибут #[ApiResource] уже создаёт ощущение поддержки.

Common mistakes

  • Сводить api platform к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет api platform через конкретную точку lifecycle в Symfony.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics