Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

API Platform

Writing controllers by hand for every entity — a list, a single record, create, update, delete — is repetitive work. API Platform builds such an API from Doctrine entities together with documentation, validation and pagination.

Installation

composer require api

A resource

Marking an entity with an attribute is enough:

use ApiPlatform\Metadata\ApiResource;
 
#[ORM\Entity]
#[ApiResource]
class Post
{
    // properties and methods
}

Five addresses appear:

Method and addressAction
GET /api/postsThe list
POST /api/postsCreating
GET /api/posts/{id}A single record
PATCH /api/posts/{id}Updating
DELETE /api/posts/{id}Deleting

The /api address opens documentation where every request can be run right in the browser.

Only the needed operations

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post as PostOperation;
 
#[ApiResource(operations: [
    new GetCollection(),
    new Get(),
    new PostOperation(),
])]
class Post
{
    // …
}

Updating and deleting through the API are no longer available.

Which fields to return and accept

Serialization groups separate reading from writing:

use Symfony\Component\Serializer\Attribute\Groups;
 
#[ApiResource(
    normalizationContext: ['groups' => ['post:read']],
    denormalizationContext: ['groups' => ['post:write']],
)]
class Post
{
    #[Groups(['post:read'])]
    private ?int $id = null;
 
    #[Groups(['post:read', 'post:write'])]
    #[Assert\NotBlank]
    private ?string $title = null;
 
    #[Groups(['post:read'])]
    private ?\DateTimeImmutable $createdAt = null;
}

The client sees the identifier and the date but cannot change them. Assert constraints are checked automatically: invalid data yields a 422 response.

Pagination

#[ApiResource(paginationItemsPerPage: 20)]

The client asks for pages with the ?page=2 parameter.

Filters

use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
 
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['title' => 'partial', 'author' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt'])]
class Post
{
    // …
}

Now the /api/posts?title=php and /api/posts?order[createdAt]=desc requests work.

Access rules

use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Patch;
 
#[ApiResource(operations: [
    new GetCollection(),
    new Get(),
    new PostOperation(security: "is_granted('ROLE_USER')"),
    new Patch(security: "object.getAuthor() == user"),
    new Delete(security: "is_granted('ROLE_ADMIN')"),
])]

In the expression object is the requested record and user is the current user.

Your own logic

Actions on saving — setting the author, sending an email — are written in a state processor class attached to an operation with the processor parameter. Data that does not come from the database is served by a state provider.

Common mistake

A bare #[ApiResource] attribute opens every operation and every property of the entity to any visitor — including deletion and fields like a password. For each resource set the list of operations, the serialization groups and the security rules, and then check in the /api documentation what a guest can see.

Thanks for reading!

Donate to the site — we work hard for you.