# Symfony 7: API Platform va Cac Thuc Hanh Tot Nhat > Huong dan day du ve API Platform 4 voi Symfony 7. Tu State Processors, State Providers den bao mat va kiem thu — tat ca thuc hanh tot nhat cho REST API san xuat. - 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 ket hop voi Symfony 7 tao ra mot nen tang xay dung REST API cap do san xuat nhanh chong va dang tin cay. Khi cac du an API phat trien theo quy mo, su khac biet giua mot API duoc xay dung dung cach va mot API chi hoat dong duoc tro nen ro rang qua cach to chuc cac processor, provider va validation logic. Bai viet nay di sau vao cac quyet dinh kien truc va pattern ma doi ngu Symfony chuyen nghiep dang su dung trong thuc te. > **API Platform 4.2 — Dieu gi moi?** > > API Platform 4.2 gioi thieu ho tro native cho PHP attributes tren State Processors, cai thien hieu suat serialization voi Symfony Serializer 7.2, va bo sung `#[ApiFilter]` inline tren cac thuoc tinh entity. Cac ung dung Symfony 7.2+ duoc huong loi tu khai bao route tu dong va giam dang ke boilerplate configuration. ## Cai Dat va Cau Hinh Ban Dau Cai dat API Platform qua Composer va thiet lap cac tuy chon co ban: ```bash # installation.sh composer create-project symfony/skeleton my-api cd my-api composer require api composer require symfony/orm-pack composer require symfony/validator composer require lexik/jwt-authentication-bundle ``` Cau hinh API Platform trong `config/packages/api_platform.yaml`: ```yaml # config/packages/api_platform.yaml api_platform: title: 'My Production API' version: '1.0.0' formats: jsonld: mime_types: ['application/ld+json'] json: mime_types: ['application/json'] jsonapi: mime_types: ['application/vnd.api+json'] docs_formats: jsonopenapi: mime_types: ['application/vnd.openapi+json'] html: mime_types: ['text/html'] defaults: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Origin'] extra_properties: standard_put: true rfc_7807_compliant_errors: true event_listeners_backward_compatibility_layer: false keep_legacy_inflector: false ``` ## API Resource Don Gian — Book Mot entity Doctrine duoc danh dau bang `#[ApiResource]` la tat ca nhung gi can thiet de co mot CRUD API day du: ```php id; } public function getTitle(): string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } public function getIsbn(): string { return $this->isbn; } public function setIsbn(string $isbn): static { $this->isbn = $isbn; return $this; } public function getPrice(): float { return $this->price; } public function setPrice(float $price): static { $this->price = $price; return $this; } } ``` > **UUID v7 cho ID phan tan** > > API Platform 4 khuyen khich su dung UUID thay vi auto-increment integer. UUID v7 theo thu tu thoi gian, cai thien hieu suat index cua cac co so du lieu nhu PostgreSQL va MySQL, dong thoi dam bao ID co the duoc tao o phia client ma khong co xung dot. ## Serialization Groups — Kiem Soat Du Lieu Tra Ve Serialization Groups cho phep kiem soat chinh xac truong nao duoc expose cho tung operation. Cau hinh qua `normalizationContext` va `denormalizationContext`: ```php ['user:list']] ), new Get( normalizationContext: ['groups' => ['user:read']] ), new Post( normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:create']] ), new Patch( normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:update']] ), ] )] class User implements PasswordAuthenticatedUserInterface { #[ORM\Id, ORM\Column(type: 'uuid'), ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] #[Groups(['user:list', 'user:read'])] private ?Uuid $id = null; #[ORM\Column(length: 180, unique: true)] #[Assert\Email] #[Assert\NotBlank] #[Groups(['user:list', 'user:read', 'user:create'])] private string $email = ''; #[ORM\Column(length: 100)] #[Assert\NotBlank] #[Groups(['user:list', 'user:read', 'user:create', 'user:update'])] private string $firstName = ''; #[ORM\Column(length: 100)] #[Groups(['user:list', 'user:read', 'user:create', 'user:update'])] private string $lastName = ''; // Chi cho phep viet — khong bao gio tra ve mat khau #[Assert\NotBlank(groups: ['user:create'])] #[Assert\Length(min: 8, groups: ['user:create', 'user:update'])] #[Groups(['user:create', 'user:update'])] private ?string $plainPassword = null; #[ORM\Column] private string $password = ''; public function getPassword(): string { return $this->password; } // ... getters and setters } ``` ## State Processors — Logic Nghiep Vu Khi Ghi State Processors xu ly logic nghiep vu truoc khi du lieu duoc luu vao co so du lieu. Chung thay the Event Listeners trong API Platform 4. ### Processor Hash Mat Khau Nguoi Dung ```php */ final class UserPasswordHasherProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private readonly ProcessorInterface $persistProcessor, private readonly UserPasswordHasherInterface $passwordHasher, ) {} public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User|null { if (!$data instanceof User || !$data->getPlainPassword()) { return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } $hashedPassword = $this->passwordHasher->hashPassword($data, $data->getPlainPassword()); $data->setPassword($hashedPassword); $data->eraseCredentials(); return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } ``` ### Processor Tuy Chinh cho Book ```php */ final class BookProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private readonly ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private readonly ProcessorInterface $removeProcessor, private readonly BookRepository $bookRepository, ) {} public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Book|null { if ($operation instanceof DeleteOperationInterface) { // Logic truoc khi xoa $this->bookRepository->archiveRelatedData($data); return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } // Tinh toan gia sau thue truoc khi luu $data->setPriceWithTax($data->getPrice() * 1.2); $data->setUpdatedAt(new \DateTimeImmutable()); return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } ``` ## State Providers — Lay Du Lieu Tuy Chinh State Providers thay the logic truy van mac dinh. Chung huu ich khi du lieu den tu nhieu nguon hoac can logic phuc tap. ### Provider Sach Pho Bien ```php */ final class PopularBooksProvider implements ProviderInterface { public function __construct( private readonly BookRepository $bookRepository, private readonly RequestStack $requestStack, ) {} /** * @return Book[] */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { $request = $this->requestStack->getCurrentRequest(); $period = $request?->query->get('period', '7days'); return match ($period) { '30days' => $this->bookRepository->findPopularLast30Days(), '90days' => $this->bookRepository->findPopularLast90Days(), default => $this->bookRepository->findPopularLast7Days(), }; } } ``` Dang ky provider trong entity: ```php ArticleGroupsGenerator::class] ), new Patch( validationContext: ['groups' => ArticleGroupsGenerator::class] ), ] )] #[Assert\Sequentially([ new Assert\NotBlank(), ])] class Article { #[ORM\Id, ORM\Column(type: 'uuid')] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] #[Groups(['article:read'])] private ?Uuid $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 5, max: 255)] #[Groups(['article:read', 'article:write'])] private string $title = ''; #[ORM\Column(type: 'text')] #[Assert\NotBlank] #[Assert\Length(min: 50)] #[Groups(['article:read', 'article:write'])] private string $content = ''; #[ORM\Column(length: 50)] #[Assert\NotBlank] #[Assert\Choice(choices: ['draft', 'review', 'published'], groups: ['article:publish'])] #[Groups(['article:read', 'article:write'])] private string $status = 'draft'; #[ORM\Column(nullable: true)] #[Assert\NotNull(groups: ['article:publish'])] #[Groups(['article:read', 'article:write'])] private ?\DateTimeImmutable $publishedAt = null; // ... getters and setters } ``` ```php getStatus()) { $groups[] = 'article:publish'; } // Admin duoc phep publish ngay lap tuc $token = $this->tokenStorage->getToken(); if ($token?->getUser() && in_array('ROLE_ADMIN', $token->getUser()->getRoles(), true)) { $groups[] = 'article:admin'; } return $groups; } } ``` > **Hieu suat Validation** > > Symfony Validator chay tat ca constraints mac dinh song song. Su dung `#[Assert\Sequentially]` khi mot constraint phu thuoc vao ket qua cua constraint truoc — vi du kiem tra dinh dang email truoc khi truy van co so du lieu. Dieu nay tranh truy van khong can thiet vao database khi du lieu dau vao da sai. ## Filters — Tim Kiem va Sap Xep API Platform cung cap nhieu filter san co. Chung co the ket hop de tao API tim kiem manh me: ```php 'partial', // /products?name=php 'category' => 'exact', // /products?category=books 'brand' => 'start', // /products?brand=sym ])] #[ApiFilter(BooleanFilter::class, properties: ['inStock'])] // /products?inStock=true #[ApiFilter(NumericFilter::class, properties: ['rating'])] // /products?rating=5 #[ApiFilter(RangeFilter::class, properties: ['price'])] // /products?price[gte]=10&price[lte]=50 #[ApiFilter(DateFilter::class, properties: ['createdAt'])] // /products?createdAt[after]=2025-01-01 #[ApiFilter(OrderFilter::class, properties: ['name', 'price', 'createdAt'], arguments: ['orderParameterName' => 'sort'])] class Product { #[ORM\Id, ORM\Column(type: 'uuid')] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] #[Groups(['product:read'])] private ?Uuid $id = null; #[ORM\Column(length: 255)] #[Groups(['product:read', 'product:write'])] private string $name = ''; #[ORM\Column] #[Groups(['product:read', 'product:write'])] private float $price = 0.0; #[ORM\Column] #[Groups(['product:read'])] private bool $inStock = true; #[ORM\Column] #[Groups(['product:read'])] private \DateTimeImmutable $createdAt; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } // ... getters and setters } ``` ## Quan He — Author va Book Xu ly dung quan he la mot trong nhung thu thach pho bien nhat khi xay dung API. Serialization Groups giai quyet van de nay mot cach gon gang: ```php ['author:list']]), new Get(normalizationContext: ['groups' => ['author:read']]), new Post(denormalizationContext: ['groups' => ['author:create']]), ] )] class Author { #[ORM\Id, ORM\Column(type: 'uuid')] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] #[Groups(['author:list', 'author:read', 'book:read'])] private ?Uuid $id = null; #[ORM\Column(length: 150)] #[Assert\NotBlank] #[Groups(['author:list', 'author:read', 'author:create', 'book:read'])] private string $name = ''; #[ORM\Column(type: 'text', nullable: true)] #[Groups(['author:read', 'author:create'])] private ?string $biography = null; /** @var Collection */ #[ORM\OneToMany(targetEntity: Book::class, mappedBy: 'author', cascade: ['persist'])] #[Groups(['author:read'])] private Collection $books; public function __construct() { $this->books = new ArrayCollection(); } // ... getters and setters } ``` ```php ['book:list']]), new Get(normalizationContext: ['groups' => ['book:read']]), new Post( denormalizationContext: ['groups' => ['book:create']], normalizationContext: ['groups' => ['book:read']] ), new Patch( denormalizationContext: ['groups' => ['book:update']], normalizationContext: ['groups' => ['book:read']] ), ] )] class Book { #[ORM\Id, ORM\Column(type: 'uuid')] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] #[Groups(['book:list', 'book:read', 'author:read'])] private ?Uuid $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Groups(['book:list', 'book:read', 'book:create', 'book:update', 'author:read'])] private string $title = ''; #[ORM\Column(length: 13, unique: true)] #[Assert\Isbn] #[Groups(['book:read', 'book:create'])] private string $isbn = ''; // Tra ve IRI hoac object day du tuy theo group #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')] #[Assert\NotNull] #[Groups(['book:list', 'book:read', 'book:create'])] private ?Author $author = null; #[ORM\Column] #[Groups(['book:read', 'book:create', 'book:update'])] private float $price = 0.0; // ... getters and setters } ``` ## Bao Mat — Kiem Soat Truy Cap Theo Nguoi Dung API Platform tich hop voi Symfony Security de bao ve resource va tung truong rieng le: ```php ['order:read']], denormalizationContext: ['groups' => ['order:write']], )] class Order { #[ORM\Id, ORM\Column(type: 'uuid')] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] #[Groups(['order:read'])] private ?Uuid $id = null; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false)] #[Groups(['order:read'])] // Chi admin moi thay owner private ?UserInterface $owner = null; #[ORM\Column(length: 50)] #[Groups(['order:read'])] private string $status = 'pending'; #[ORM\Column] #[Assert\Positive] #[Groups(['order:read', 'order:write'])] private float $total = 0.0; #[ORM\Column] #[Groups(['order:read'])] private \DateTimeImmutable $createdAt; // Truong nay chi admin moi thay — dung security expression tren property #[ORM\Column(type: 'text', nullable: true)] #[Groups(['order:admin:read'])] private ?string $internalNotes = null; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } public function getOwner(): ?UserInterface { return $this->owner; } public function setOwner(?UserInterface $owner): static { $this->owner = $owner; return $this; } public function getStatus(): string { return $this->status; } // ... other getters and setters } ``` ## Kiem Thu — Dam Bao API Hoat Dong Dung API Platform cung cap `ApiTestCase` de kiem thu toan bo HTTP stack, bao gom authentication, validation va format phan hoi: ```php request('GET', '/api/books'); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@id' => '/api/books', '@type' => 'hydra:Collection', 'hydra:totalItems' => 3, ]); $this->assertCount(3, $response->toArray()['hydra:member']); } public function testCreateBookRequiresAuthentication(): void { static::createClient()->request('POST', '/api/books', ['json' => [ 'title' => 'Test Book', 'isbn' => '978-3-16-148410-0', 'price' => 29.99, ]]); $this->assertResponseStatusCodeSame(401); } public function testCreateBook(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); $token = $this->getJwtToken($user->object()); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $token, 'json' => [ 'title' => 'Symfony 7 in Practice', 'isbn' => '978-3-16-148410-0', 'price' => 39.99, ], ]); $this->assertResponseStatusCodeSame(201); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'Symfony 7 in Practice', 'isbn' => '978-3-16-148410-0', 'price' => 39.99, ]); $this->assertMatchesResourceItemJsonSchema(Book::class); } public function testUpdateBook(): void { $book = BookFactory::createOne(['price' => 29.99]); $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); $token = $this->getJwtToken($user->object()); static::createClient()->request('PATCH', '/api/books/'.$book->getId(), [ 'auth_bearer' => $token, 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => ['price' => 34.99], ]); $this->assertResponseIsSuccessful(); $this->assertJsonContains(['price' => 34.99]); } public function testDeleteBook(): void { $book = BookFactory::createOne(); $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); $token = $this->getJwtToken($user->object()); static::createClient()->request('DELETE', '/api/books/'.$book->getId(), [ 'auth_bearer' => $token, ]); $this->assertResponseStatusCodeSame(204); } private function getJwtToken(object $user): string { $client = static::createClient(); $response = $client->request('POST', '/api/auth', ['json' => [ 'email' => $user->getEmail(), 'password' => 'password', ]]); return $response->toArray()['token']; } } ``` ## Ket Luan API Platform 4 voi Symfony 7 cung cap mot bo cong cu day du de xay dung REST API san xuat. Viec nam vung cac State Processors, State Providers, Serialization Groups va Filters cho phep xay dung cac API linh hoat, bao mat va de bao tri. ### Danh Sach Kiem Tra Thuc Hanh Tot Nhat - Dung UUID thay auto-increment ID de ho tro cac he thong phan tan - Dinh nghia Serialization Groups ro rang cho tung operation (list/read/write) - Xu ly toan bo logic nghiep vu trong State Processors thay vi Event Listeners - Dung State Providers khi du lieu can duoc lay tu nhieu nguon - Ket hop `#[Assert\Sequentially]` khi cac constraint phu thuoc lan nhau - Bao ve tung operation rieng le bang `security` expression trong `#[ApiResource]` - Viet functional tests bang `ApiTestCase` voi Foundry factories - Bat `rfc_7807_compliant_errors: true` de co response loi chuan hoa Cac du an duoc xay dung theo cac nguyen tac nay se co kien truc ro rang, de kiem thu va san sang scale theo nhu cau san xuat. API Platform khong chi la mot cong cu tiet kiem thoi gian — no la mot kien truc API duoc suy nghi ky luong cho he sinh thai PHP hien dai. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/symfony/symfony-7-api-platform-best-practices