Skip to content

Latest commit

 

History

History
195 lines (145 loc) · 6.69 KB

File metadata and controls

195 lines (145 loc) · 6.69 KB

Quick Start

Build a JSON API endpoint in four steps.


1. Install the bundle

composer require gearsite/api

Symfony Flex registers the bundle for you. The recipe writes this line in config/bundles.php:

Gearsite\Api\GearsiteJSONAPIBundle::class => ['all' => true],

NOTE: If you do not use Flex, add that line yourself. Flex configures a package only from its recipe. This package has one.


2. Create a serializer

Run the maker command:

php bin/console make:api-serializer

Open the new file. Then declare the fields and relationships:

use Gearsite\Api\Serializer\Descriptor\Attribute;
use Gearsite\Api\Serializer\Descriptor\Relationship;
use Gearsite\Api\Serializer\DoctrineSerializer;
use Gearsite\Api\Serializer\SerializerRegistry;

class ArticleSerializer extends DoctrineSerializer {

    public function __construct(
        ArticleRepository $repository,
        private readonly SerializerRegistry $registry,
    ) {
        parent::__construct(type: 'article', repository: $repository);
    }

    public function getFields(): array {
        return [
            'title'      => Attribute::native(),
            'body'       => Attribute::native(),
            'created-at' => Attribute::native('createdAt'),
        ];
    }

    public function getRelationships(): array {
        return [
            'comments' => Relationship::toMany($this->registry->get(CommentSerializer::class)),
        ];
    }
}

Attribute::native() maps a field to the entity property with the same name. To map a property with a different name, pass that name: Attribute::native('createdAt').

Get related serializers from SerializerRegistry. Serializers often reference each other, and the registry prevents circular constructor dependencies.

NOTE: For computed fields, secured fields, and relationship options, read serializers.md.


3. Implement the query generator

Generator is abstract. Extend it once and implement both abstract methods:

use Gearsite\Api\Query\Generator;

class QueryGenerator extends Generator {

    protected function buildPagination(): void {
        $page   = $this->request->getPageParameters();
        $number = max(1, intval($page['number'] ?? 1));
        $size   = max(1, intval($page['size'] ?? 25));
        $this->qb->setFirstResult(($number - 1) * $size)->setMaxResults($size);
    }

    protected function buildFilter(): void {
        // Read $this->request->getFilterParameters() and apply them to $this->qb.
    }
}

The JSON API specification leaves pagination and filtering to the implementation. Both methods are required, even when one of them does nothing.

CAUTION: Bind every filter value as a query parameter. Never concatenate request input into DQL.


4. Build the controller

use Gearsite\Api\Factory\DocumentFactory;
use Gearsite\Api\Factory\QueryFactory;
use Gearsite\Api\Model\Document;
use Gearsite\Api\Request\SymfonyRequest;

#[Route('/articles')]
class ArticleController extends AbstractController {

    public function __construct(
        private readonly ArticleSerializer $serializer,
        private readonly DocumentFactory $documentFactory,
        private readonly QueryFactory $queryFactory,
        private readonly EntityManagerInterface $em,
        private readonly Security $security,
    ) {}

    #[Route('', methods: ['GET'])]
    public function index(SymfonyRequest $request): JsonResponse {
        $results  = $this->queryFactory->create(QueryGenerator::class, serializer: $this->serializer, request: $request, security: $this->security)->build()->execute();
        $document = $this->documentFactory->create(data: $results, serializer: $this->serializer, request: $request);
        return new JsonResponse(data: $document, status: 200, headers: ['Content-Type' => Document::MEDIA_TYPE]);
    }

    #[Route('/{id}', methods: ['GET'])]
    public function show(int $id, SymfonyRequest $request): JsonResponse {
        $article  = $this->queryFactory->create(QueryGenerator::class, serializer: $this->serializer, request: $request, security: $this->security, id: $id)->build()->execute();
        $document = $this->documentFactory->create(data: $article, serializer: $this->serializer, request: $request);
        return new JsonResponse(data: $document, status: 200, headers: ['Content-Type' => Document::MEDIA_TYPE]);
    }

    #[Route('', methods: ['POST'])]
    public function create(SymfonyRequest $request): JsonResponse {
        $article = new Article();
        $this->serializer->deserialize($request->getContent(), $article);
        $this->em->persist($article);
        $this->em->flush();
        $document = $this->documentFactory->create(data: $article, serializer: $this->serializer, request: $request);
        return new JsonResponse(data: $document, status: 201, headers: ['Content-Type' => Document::MEDIA_TYPE]);
    }

    #[Route('/{id}', methods: ['PATCH'])]
    public function update(int $id, SymfonyRequest $request): JsonResponse {
        $article = $this->serializer->getRepository()->find($id);
        $this->serializer->deserialize($request->getContent(), $article);
        $this->em->flush();
        $document = $this->documentFactory->create(data: $article, serializer: $this->serializer, request: $request);
        return new JsonResponse(data: $document, status: 200, headers: ['Content-Type' => Document::MEDIA_TYPE]);
    }

    #[Route('/{id}', methods: ['DELETE'])]
    public function delete(int $id): Response {
        $article = $this->serializer->getRepository()->find($id);
        $this->em->remove($article);
        $this->em->flush();
        return new Response(status: 204);
    }
}

The container injects SymfonyRequest for you. It extends the Symfony Request class and adds the JSON API query parameters.

deserialize() applies a request body onto the entity with PATCH semantics. It writes only the fields that the body contains.


Try the endpoint

# List articles
curl http://localhost/articles

# Sparse fieldset and include
curl "http://localhost/articles?fields[article]=title,body&include=comments"

# Create
curl -X POST http://localhost/articles \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data":{"type":"article","attributes":{"title":"Hello","body":"World"}}}'

# Update
curl -X PATCH http://localhost/articles/1 \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data":{"type":"article","id":"1","attributes":{"title":"Updated"}}}'

Next steps