campsjos
campsjos

Reputation: 1405

How can i limit the number of nested entities in API Platform?

Having two related entities, let's say Author and Book, I can limit (or paginate) the results of Authors but not the number of results of its related entity Books which always shows the whole collection.

The issue is that Authors may have hundreds of Books making the resulting JSON huge and heavy to parse so I'm trying to get, for example, only the last 5 books.

I'm sure I'm missing something since I think this is probably a common scenario but I can't find anything on the docs nor here in StackOverflow.

I'm starting with Api Platform, any hint would be appreciated!

Upvotes: 2

Views: 2071

Answers (1)

campsjos
campsjos

Reputation: 1405

I finally solved it creating a normalizer for the entity but I still think that it has to be a simpler solution.

Here's what I had to do, following the Authors / Books example:

Add a setter to the Author entity to override the Author's Book collection:

// src/Entity/Author.php

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
// ...

/**
 * @ApiResource
 * @ORM\Entity(repositoryClass="App\Repository\AuthorRepository")
 */
class Author
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Book", mappedBy="author", orphanRemoval=true)
     */
    private $books;

    public function __construct()
    {
        $this->books = new ArrayCollection();
    }

    // Getters and setters
    //...

    public function setBooks($books): self
    {
        $this->books = $books;

        return $this;
    }

}

Create a normalizer for the Author's entity:

// App/Serializer/Normalizer/AuthorNormalizer.php

<?php

namespace App\Serializer\Normalizer;

use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;

class AuthorNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
    use SerializerAwareTrait;

    private $normalizer;

    public function __construct(
        NormalizerInterface $normalizer,
        IriConverterInterface $iriConverter
    ) {
        if (!$normalizer instanceof DenormalizerInterface) {
            throw new \InvalidArgumentException('The normalizer must implement the DenormalizerInterface');
        }
        if (!$normalizer instanceof AbstractItemNormalizer) {
            throw new \InvalidArgumentException('The normalizer must be an instance of AbstractItemNormalizer');
        }
        $handler = function ($entity) use ($iriConverter) {
            return $iriConverter->getIriFromItem($entity);
        };
        $normalizer->setMaxDepthHandler($handler);
        $normalizer->setCircularReferenceHandler($handler);
        $this->normalizer = $normalizer;
    }

    public function denormalize($data, $class, $format = null, array $context = [])
    {
        return $this->normalizer->denormalize($data, $class, $format, $context);
    }

    public function supportsDenormalization($data, $type, $format = null)
    {
        return $this->normalizer->supportsDenormalization($data, $type, $format);
    }

    public function normalize($object, $format = null, array $context = [])
    {
        // Number of desired Books to list
        $limit = 2;
        $newBooksCollection = new ArrayCollection();
        $books = $object->getBooks();
        $booksCount = count($books);
        if ($booksCount > $limit) {

            // Reverse iterate the original Book collection as I just want the last ones
            for ($i = $booksCount; $i > $booksCount - $limit; $i--) {
                $newBooksCollection->add($books->get($i - 1));
            }
        }

        // Setter previously added to the Author entity to override its related Books
        $object->setBooks($newBooksCollection);
        $data = $this->normalizer->normalize($object, $format, $context);

        return $data;
    }

    public function supportsNormalization($data, $format = null)
    {
        return $data instanceof \App\Entity\Author;
    }
}

And finally register the normalizer as a service manually (using autowire led me to circular reference issues):

services:
    App\Serializer\Normalizer\AuthorNormalizer:
        autowire: false
        autoconfigure: true
        arguments:
            $normalizer: '@api_platform.jsonld.normalizer.item'
            $iriConverter: '@ApiPlatform\Core\Api\IriConverterInterface'

Upvotes: 1

Related Questions