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.

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 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:
# 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-bundleCau hinh API Platform trong config/packages/api_platform.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: falseAPI 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
// src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(),
new Post(),
new Get(),
new Put(),
new Patch(),
new Delete(),
]
)]
class Book
{
#[ORM\Id]
#[ORM\Column(type: 'uuid', unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?Uuid $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 255)]
private string $title = '';
#[ORM\Column(length: 13, unique: true)]
#[Assert\Isbn]
private string $isbn = '';
#[ORM\Column]
private float $price = 0.0;
public function getId(): ?Uuid { return $this->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; }
}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
// src/Entity/User.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(
normalizationContext: ['groups' => ['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
}Sẵn sàng chinh phục phỏng vấn Symfony?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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
// src/State/UserPasswordHasherProcessor.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* @implements ProcessorInterface<User, User|void>
*/
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
// src/State/BookProcessor.php
namespace App\State;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use App\Repository\BookRepository;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* @implements ProcessorInterface<Book, Book|void>
*/
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
// src/State/PopularBooksProvider.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Book;
use App\Repository\BookRepository;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @implements ProviderInterface<Book>
*/
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
// src/Entity/Book.php (trich doan them vao)
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/books/popular',
provider: PopularBooksProvider::class,
),
new GetCollection(),
new Get(),
new Post(
processor: BookProcessor::class,
),
new Patch(
processor: BookProcessor::class,
),
new Delete(
processor: BookProcessor::class,
),
]
)]
class Book
{
// ... (nhu tren)
}Validation Nang Cao voi Constraint Groups
API Platform cho phep dieu chinh cac nhom validation theo tung operation. Dieu nay rat quan trong khi cac quy tac tao va cap nhat khac nhau.
<?php
// src/Entity/Article.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Validator\ArticleGroupsGenerator;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(
validationContext: ['groups' => 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
// src/Validator/ArticleGroupsGenerator.php
namespace App\Validator;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
use App\Entity\Article;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
final class ArticleGroupsGenerator implements ValidationGroupsGeneratorInterface
{
public function __construct(
private readonly TokenStorageInterface $tokenStorage,
) {}
/**
* @return string[]
*/
public function __invoke(object $object, Operation $operation): array
{
/** @var Article $object */
$groups = ['Default', 'article:write'];
// Bao sung nhom publish neu status la published
if ('published' === $object->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;
}
}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
// src/Entity/Product.php
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\NumericFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
operations: [new GetCollection()]
)]
#[ApiFilter(SearchFilter::class, properties: [
'name' => '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
// src/Entity/Author.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(normalizationContext: ['groups' => ['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<int, Book> */
#[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
// src/Entity/Book.php (voi quan he Author)
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(normalizationContext: ['groups' => ['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
// src/Entity/Order.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(
// Chi tra ve don hang cua nguoi dung hien tai
security: 'is_granted("ROLE_USER")',
),
new Post(
security: 'is_granted("ROLE_USER")',
securityMessage: 'Ban phai dang nhap de dat hang.',
),
new Get(
// Nguoi dung chi xem don hang cua minh, admin xem tat ca
security: 'is_granted("ROLE_ADMIN") or object.getOwner() == user',
securityMessage: 'Ban khong co quyen xem don hang nay.',
),
new Patch(
security: 'is_granted("ROLE_ADMIN") or (object.getOwner() == user and object.getStatus() == "pending")',
securityMessage: 'Chi co the cap nhat don hang dang cho xu ly.',
),
new Delete(
security: 'is_granted("ROLE_ADMIN")',
securityMessage: 'Chi quan tri vien moi co the xoa don hang.',
),
],
normalizationContext: ['groups' => ['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
// tests/Functional/BookTest.php
namespace App\Tests\Functional;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Entity\Book;
use App\Factory\BookFactory;
use App\Factory\UserFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
final class BookTest extends ApiTestCase
{
use Factories;
use ResetDatabase;
public function testGetCollection(): void
{
BookFactory::createMany(3);
$response = static::createClient()->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
securityexpression trong#[ApiResource] - Viet functional tests bang
ApiTestCasevoi Foundry factories - Bat
rfc_7807_compliant_errors: truede co response loi chuan hoa
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
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.
Thẻ
Chia sẻ
