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.

Huong dan API Platform voi Symfony 7 — Cac Thuc Hanh Tot Nhat 2026

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
<?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; }
}
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
<?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
<?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
<?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
<?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
<?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
<?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
<?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;
    }
}
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
<?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
<?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
<?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
<?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
<?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 security expression trong #[ApiResource]
  • Viet functional tests bang ApiTestCase voi Foundry factories
  • Bat rfc_7807_compliant_errors: true de 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ẻ

#symfony
#api platform
#php
#rest api
#api development

Chia sẻ

Bài viết liên quan