# Symfony 7 та API Platform: найкращі практики розробки REST API > Повний практичний посібник з API Platform 4 та Symfony 7: State Processors, State Providers, серіалізація, фільтри, безпека та тестування REST API на PHP. - Published: 2026-01-12 - Updated: 2026-04-07 - Author: SharpSkill - Tags: symfony, api platform, php, rest api, api development - Reading time: 14 min --- API Platform 4 кардинально змінює підхід до створення REST та GraphQL API у Symfony 7. Нова версія впроваджує чітке розмежування відповідальності, спрощені State Providers та Processors, а також нативну інтеграцію з Symfony Object Mapper. Побудова професійного API стала значно інтуїтивнішою. > **Що нового в API Platform 4.2** > > Версія 4.2 представляє JSON Streamer з приростом продуктивності до +32% RPS, повністю перероблену систему фільтрів та Mutators для кастомізації операцій без модифікації ядра. Підтримка Symfony 7 та 8 вбудована нативно. ## Встановлення та початкове налаштування API Platform встановлюється кількома командами через Symfony Flex. Стандартна конфігурація покриває більшість сценаріїв використання і водночас залишається повністю налаштовуваною. ```bash # terminal # Create a new Symfony project with API Platform composer create-project symfony/skeleton my-api cd my-api # Install API Platform with Doctrine ORM composer require api # Verify installation php bin/console debug:router | grep api ``` Symfony Flex автоматично налаштовує маршрути, OpenAPI-документацію та інтерфейс Swagger UI, доступний за адресою `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' # Supported response formats formats: jsonld: ['application/ld+json'] json: ['application/json'] # OpenAPI documentation swagger: versions: [3] # Default pagination defaults: pagination_items_per_page: 30 pagination_maximum_items_per_page: 100 ``` Ця конфігурація визначає формати серіалізації, глобальну пагінацію та метадані документації API. ## Створення простого API-ресурсу Атрибут `#[ApiResource]` оголошує Doctrine-сутність як REST-ресурс. API Platform автоматично генерує CRUD-ендпоінти, OpenAPI-документацію та базові валідації. ```php 'DESC'], // Pagination configuration for this resource paginationItemsPerPage: 20 )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank(message: 'Title is required')] #[Assert\Length(min: 2, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] private ?string $description = null; #[ORM\Column(length: 13, unique: true)] #[Assert\Isbn] private ?string $isbn = null; #[ORM\Column] private ?\DateTimeImmutable $publishedAt = null; // Getters and setters... public function getId(): ?int { return $this->id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(string $description): static { $this->description = $description; return $this; } public function getIsbn(): ?string { return $this->isbn; } public function setIsbn(string $isbn): static { $this->isbn = $isbn; return $this; } public function getPublishedAt(): ?\DateTimeImmutable { return $this->publishedAt; } public function setPublishedAt(\DateTimeImmutable $publishedAt): static { $this->publishedAt = $publishedAt; return $this; } } ``` Ця сутність генерує шість ендпоінтів: `GET /api/books`, `POST /api/books`, `GET /api/books/{id}`, `PUT /api/books/{id}`, `PATCH /api/books/{id}` та `DELETE /api/books/{id}`. > **Рекомендовано використовувати UUID v7** > > API Platform нативно підтримує UUID v7 як ідентифікатори. Такий підхід покращує безпеку (непередбачувані ідентифікатори) та продуктивність (природне сортування за датою створення). ## Групи серіалізації для контролю даних, що передаються Групи серіалізації точно визначають, які властивості відкриваються під час читання (нормалізація) та запису (денормалізація). Це розмежування є ключовим для безпеки та гнучкості API. ```php ['Default', 'user:create']]), new Get(), new Put(processor: UserPasswordHasher::class), new Patch(processor: UserPasswordHasher::class), new Delete(), ], // Properties exposed when reading normalizationContext: ['groups' => ['user:read']], // Properties accepted when writing denormalizationContext: ['groups' => ['user:create', 'user:update']], )] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:read'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] #[Assert\NotBlank] #[Assert\Email] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $email = null; #[ORM\Column] private ?string $password = null; // Never exposed when reading, only when writing #[Assert\NotBlank(groups: ['user:create'])] #[Groups(['user:create', 'user:update'])] private ?string $plainPassword = null; #[ORM\Column(length: 100)] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $fullName = null; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; #[ORM\Column] #[Groups(['user:read'])] private ?\DateTimeImmutable $createdAt = null; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } // UserInterface implementation public function getUserIdentifier(): string { return (string) $this->email; } public function getRoles(): array { $roles = $this->roles; $roles[] = 'ROLE_USER'; return array_unique($roles); } public function getPassword(): string { return $this->password; } public function eraseCredentials(): void { $this->plainPassword = null; } // Getters and setters... public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): static { $this->email = $email; return $this; } public function setPassword(string $password): static { $this->password = $password; return $this; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): static { $this->plainPassword = $plainPassword; return $this; } public function getFullName(): ?string { return $this->fullName; } public function setFullName(string $fullName): static { $this->fullName = $fullName; return $this; } public function setRoles(array $roles): static { $this->roles = $roles; return $this; } public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; } } ``` Завдяки цій конфігурації `plainPassword` ніколи не з'являється у відповідях, але може бути надісланий під час створення або оновлення. ## State Processors для інкапсуляції бізнес-логіки State Processors перехоплюють операції збереження даних і додають бізнес-логіку. API Platform 4 спрощує їхнє створення завдяки ін'єкції залежностей через атрибути. ```php */ final class UserPasswordHasher implements ProcessorInterface { public function __construct( // Injection of standard Doctrine processor #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, private UserPasswordHasherInterface $passwordHasher, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User { // Hash password if provided if ($data->getPlainPassword()) { $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); } // Delegate persistence to standard processor return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } ``` Паттерн композиції дозволяє додавати будь-яку логіку (надсилання листів, події, логування), зберігаючи стандартну поведінку збереження. ### Processor з умовною логікою Processor може адаптувати поведінку залежно від типу операції. ```php */ final class BookProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private ProcessorInterface $removeProcessor, private NotificationService $notifications, private SearchIndexer $searchIndexer, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // Deletion: use remove processor if ($operation instanceof DeleteOperationInterface) { $this->searchIndexer->remove($data); return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } // Creation: set publication date if ($operation instanceof Post) { $data->setPublishedAt(new \DateTimeImmutable()); } // Standard persistence $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); // Post-processing: indexing and notification $this->searchIndexer->index($result); if ($operation instanceof Post) { $this->notifications->notifyNewBook($result); } return $result; } } ``` ## State Providers для роботи з довільними джерелами даних State Providers отримують дані з будь-якого джерела: зовнішніх API, кешу, файлів або складної бізнес-логіки. ```php */ final class PopularBooksProvider implements ProviderInterface { public function __construct( private BookRepository $bookRepository, private CacheInterface $cache, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { // 5-minute cache for popular books return $this->cache->get('popular_books', function (ItemInterface $item) { $item->expiresAfter(300); return $this->bookRepository->findPopular(limit: 10); }); } } ``` Цей провайдер використовується у виділеній операції. ```php ['Default', 'article:create']]), // More lenient validation for updates new Put(validationContext: ['groups' => ['Default', 'article:update']]), ], )] class Article { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 10, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] // Minimum 500 characters on creation #[Assert\Length(min: 500, groups: ['article:create'])] // Minimum 100 characters for updates #[Assert\Length(min: 100, groups: ['article:update'])] private ?string $content = null; #[ORM\Column(length: 50)] #[Assert\NotBlank(groups: ['article:create'])] #[Assert\Choice(choices: ['draft', 'published', 'archived'])] private ?string $status = 'draft'; #[ORM\Column(nullable: true)] // Required only if status is "published" #[Assert\NotBlank(groups: ['article:publish'])] private ?\DateTimeImmutable $publishedAt = null; // Getters and setters... } ``` ### Динамічна валідація через сервіс Для складних правил валідації кастомний генератор груп надає повну гнучкість. ```php security->isGranted('ROLE_ADMIN')) { $groups[] = 'admin'; return $groups; } // Additional validation if publishing if ($object->getStatus() === 'published') { $groups[] = 'article:publish'; } return $groups; } } ``` > **Продуктивність при валідації** > > Складні валідації можуть впливати на продуктивність. Для масового імпорту даних варто розглянути тимчасове відключення окремих валідацій або використання асинхронних обмежень. ## Фільтри для гнучких запитів API Platform 4.2 повністю переосмислює систему фільтрів з чітким розмежуванням відповідальності. Фільтри дозволяють клієнтам API здійснювати пошук та сортування даних. ```php 'partial', // LIKE %value% 'description' => 'partial', 'category.name' => 'exact', // Search on relation 'sku' => 'exact', // Exact match ])] // Range filtering #[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])] // Date filtering #[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])] // Boolean filtering #[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])] // Customizable sorting #[ApiFilter(OrderFilter::class, properties: [ 'name', 'price', 'createdAt', ], arguments: ['orderParameterName' => 'sort'])] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column(length: 50, unique: true)] private ?string $sku = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] private ?string $price = null; #[ORM\Column] private ?int $stock = null; #[ORM\Column] private ?bool $isActive = true; #[ORM\Column] private ?bool $isFeatured = false; #[ORM\ManyToOne(targetEntity: Category::class)] private ?Category $category = null; #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; // Getters and setters... } ``` Ці фільтри автоматично генерують OpenAPI-документацію та вмикають запити на кшталт: ``` GET /api/products?name=phone&price[gte]=100&price[lte]=500&isActive=true&sort[price]=asc ``` ## Відносини та підресурси API Platform елегантно обробляє зв'язки між сутностями з опціями серіалізації та підресурсами. ```php ['author:read']], )] class Author { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['author:read', 'book:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['author:read', 'book:read'])] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] #[Groups(['author:read'])] private ?string $biography = null; #[ORM\OneToMany(mappedBy: 'author', targetEntity: Book::class)] #[Groups(['author:read'])] private Collection $books; public function __construct() { $this->books = new ArrayCollection(); } // Getters and setters... } ``` ```php ['book:read']], )] // Subresource: GET /api/authors/{authorId}/books #[ApiResource( uriTemplate: '/authors/{authorId}/books', operations: [new GetCollection()], uriVariables: [ 'authorId' => new Link( fromProperty: 'books', fromClass: Author::class ), ], normalizationContext: ['groups' => ['book:read']], )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['book:read', 'author:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['book:read', 'author:read'])] private ?string $title = null; #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')] #[ORM\JoinColumn(nullable: false)] #[Groups(['book:read'])] private ?Author $author = null; // Getters and setters... } ``` ## Безпека та контроль доступу API Platform безшовно інтегрується з системою безпеки Symfony. Voters та вирази безпеки контролюють доступ до ресурсів. ```php customer; } } ``` Вираз `object.getCustomer() == user` надає доступ до поточної сутності та авторизованого користувача для детальних перевірок. ## Автоматизоване тестування API API Platform надає PHPUnit traits для зручного тестування ендпоінтів. ```php request('GET', '/api/books'); // Assertions $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@type' => 'Collection', 'totalItems' => 30, ]); // Verify pagination (20 items per page) $this->assertCount(20, $response->toArray()['member']); } public function testCreateBook(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => 'Clean Code', 'description' => 'A Handbook of Agile Software Craftsmanship', 'isbn' => '9780132350884', ], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'Clean Code', ]); } public function testCreateBookValidationFails(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => '', // Empty title = error 'isbn' => 'invalid-isbn', ], ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ '@type' => 'ConstraintViolationList', 'violations' => [ ['propertyPath' => 'title', 'message' => 'Title is required'], ], ]); } private function getToken(object $user): string { // Implementation depends on your authentication system return 'test_token'; } } ``` ## Висновок API Platform 4 разом із Symfony 7 представляє найсучасніший підхід до створення професійних REST API на PHP. Чітке розмежування між State Providers (читання) та State Processors (запис), поєднане з групами серіалізації та системою валідації, дозволяє будувати надійні та підтримувані API. ### Чеклист для якісного API - Використовувати окремі групи серіалізації для читання та запису - Реалізовувати State Processors для бізнес-логіки (хешування паролів, сповіщення) - Налаштовувати фільтри для пошуку та сортування - Застосовувати валідації на рівні операцій з групами - Захищати ендпоінти за допомогою виразів безпеки - Покривати кожен ендпоінт функціональними тестами - Документувати API через метадані OpenAPI Філософія API Platform 4 надає перевагу композиції над наслідуванням та конфігурації над конвенцією. Результат — масштабовані, тестовані API, що відповідають стандартам REST/JSON-LD та готові до продакшн-розгортання з першого дня. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/symfony/symfony-7-api-platform-best-practices