📖 Symfony Framework Toàn tập - API Platform
70 phút

API Platform

Giới thiệu

API Platform giúp tạo REST API nhanh chóng với ít code.

Cài đặt

composer require api

Entity với API Resource

<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
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(security: "is_granted('ROLE_USER')"),
        new Put(security: "is_granted('ROLE_USER') and object.getAuthor() == user"),
        new Delete(security: "is_granted('ROLE_ADMIN')"),
    ],
    normalizationContext: ['groups' => ['product:read']],
    denormalizationContext: ['groups' => ['product:write']],
    paginationItemsPerPage: 20,
)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['product:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 200)]
    #[Assert\NotBlank]
    #[Groups(['product:read', 'product:write'])]
    private string $name;

    #[ORM\Column(type: 'text', nullable: true)]
    #[Groups(['product:read', 'product:write'])]
    private ?string $description = null;

    #[ORM\Column(type: 'decimal', precision: 12, scale: 2)]
    #[Assert\Positive]
    #[Groups(['product:read', 'product:write'])]
    private string $price;

    #[ORM\Column]
    #[Assert\PositiveOrZero]
    #[Groups(['product:read', 'product:write'])]
    private int $stock = 0;

    #[ORM\Column(type: 'datetime_immutable')]
    #[Groups(['product:read'])]
    private \DateTimeImmutable $createdAt;

    #[ORM\ManyToOne(inversedBy: 'products')]
    #[Groups(['product:read', 'product:write'])]
    private ?Category $category = null;

    public function __construct()
    {
        $this->createdAt = new \DateTimeImmutable();
    }

    // Getters/Setters...
}

Auto-generated endpoints

Khi bạn tạo ApiResource, các endpoints sau sẽ tự động có:

  • GET /api/products - list with pagination
  • GET /api/products/{id} - get one
  • POST /api/products - create
  • PUT /api/products/{id} - update
  • PATCH /api/products/{id} - partial update
  • DELETE /api/products/{id} - delete

Filtering và Sorting

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

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
    'name' => 'partial',
    'category.id' => 'exact',
    'price' => 'exact',
])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'price'])]
class Product
{
    // ...
}

Query examples

GET /api/products?name=laptop
GET /api/products?category.id=5
GET /api/products?order[price]=desc
GET /api/products?page=2

Custom Operations

#[ApiResource]
class Product
{
    #[Get(
        uriTemplate: '/products/{id}/related',
        controller: RelatedProductsController::class
    )]
    public function related(): array
    {
        return [];
    }
}

Serialization Groups

#[ApiResource(
    normalizationContext: ['groups' => ['product:read']],
    denormalizationContext: ['groups' => ['product:write']],
)]
class Product
{
    #[Groups(['product:read'])]
    private ?int $id = null;

    #[Groups(['product:read', 'product:write'])]
    private string $name;
}

DTOs

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(
    stateOptions: new Options(
        itemUriTemplate: '/products/{id}',
        collectionUriTemplate: '/products',
    )
)]
class ProductInput
{
    public string $name;
    public string $price;
}

Security

#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(
            security: "is_granted('ROLE_USER')",
            securityMessage: "Only authenticated users can create products"
        ),
        new Put(
            security: "is_granted('EDIT', object)",
            securityMessage: "You can only edit your own products"
        ),
        new Delete(
            security: "is_granted('ROLE_ADMIN')",
            securityMessage: "Only admins can delete products"
        ),
    ],
)]
class Product {}

JWT Integration

composer require lexik/jwt-authentication-bundle

Generate keys

php bin/console lexik:jwt:generate-keypair

Config

# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600

security.yaml

security:
    firewalls:
        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern: ^/api
            stateless: true
            jwt: ~

routes

api_login_check:
    path: /api/login_check

Testing

# Test API with curl
curl -X GET http://localhost:8000/api/products

curl -X POST http://localhost:8000/api/products \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -d '{"name":"New Product","price":"99.99","stock":10}'

OpenAPI Docs

Mặc định API Platform tạo Swagger UI tại /api/docs.

Bài tập thực hành

Hãy expose entity Product qua API Platform!

📝 Bài tập (1)

  1. Expose Product qua REST API