Guides
Guide

Custom pagination

expert
In case you’re using a custom collection (through a Provider), make sure you return the Paginator object to get the full hydra response with hydra:view (which contains information about first, last, next and previous page). The following example shows how to handle it using a custom Provider. You will need to use the Doctrine Paginator and pass it to the API Platform Paginator.
// src/App/Entity.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use App\Repository\BookRepository;
use App\State\BooksListProvider;
use Doctrine\ORM\Mapping as ORM;
/* Use custom Provider on operation to retrieve the custom collection */
#[ApiResource(
    operations: [
        new GetCollection(provider: BooksListProvider::class),
    ]
)]
#[ORM\Entity(repositoryClass: BookRepository::class)]
class Book
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    public ?int $id = null;
    #[ORM\Column]
    public ?string $title = null;
    #[ORM\Column(name: 'is_published', type: 'boolean')]
    public ?bool $published = null;
}

// src/App/Repository.php
namespace App\Repository;
use App\Entity\Book;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
use Doctrine\Persistence\ManagerRegistry;
class BookRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Book::class);
    }
    public function getPublishedBooks(int $page = 1, int $itemsPerPage = 30): DoctrinePaginator
    {
        /* Retrieve the custom collection and inject it into a Doctrine Paginator object */
        return new DoctrinePaginator(
            $this->createQueryBuilder('b')
                 ->where('b.published = :isPublished')
                 ->setParameter('isPublished', true)
                 ->addCriteria(
                     Criteria::create()
                         ->setFirstResult(($page - 1) * $itemsPerPage)
                         ->setMaxResults($itemsPerPage)
                 )
        );
    }
}

// src/App/State.php
namespace App\State;
use ApiPlatform\Doctrine\Orm\Paginator;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\ProviderInterface;
use App\Repository\BookRepository;
class BooksListProvider implements ProviderInterface
{
    public function __construct(private readonly BookRepository $bookRepository, private readonly Pagination $pagination)
    {
    }
    public function provide(Operation $operation, array $uriVariables = [], array $context = []): Paginator
    {
        /* Retrieve the pagination parameters from the context thanks to the Pagination object */
        [$page, , $limit] = $this->pagination->getPagination($operation, $context);
        /* Decorates the Doctrine Paginator object to the API Platform Paginator one */
        return new Paginator($this->bookRepository->getPublishedBooks($page, $limit));
    }
}

// src/App/Playground.php
namespace App\Playground;
use Symfony\Component\HttpFoundation\Request;
function request(): Request
{
    return Request::create('/books.jsonld', 'GET');
}

// src/DoctrineMigrations.php
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Migration extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, is_published SMALLINT NOT NULL)');
    }
}

// src/App/Fixtures.php
namespace App\Fixtures;
use App\Entity\Book;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Zenstruck\Foundry\AnonymousFactory;
use function Zenstruck\Foundry\faker;
final class BookFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        /* Create books published or not */
        $factory = AnonymousFactory::new(Book::class);
        $factory->many(5)->create(static function (int $i): array {
            return [
                'title' => faker()->title(),
                'published' => false,
            ];
        });
        $factory->many(35)->create(static function (int $i): array {
            return [
                'title' => faker()->title(),
                'published' => true,
            ];
        });
    }
}

// src/App/Tests.php
namespace App\Tests;
use ApiPlatform\Playground\Test\TestGuideTrait;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;
final class BookTest extends ApiTestCase
{
    use TestGuideTrait;
    public function testTheCustomCollectionIsPaginated(): void
    {
        $response = static::createClient()->request('GET', '/books.jsonld');
        $this->assertResponseIsSuccessful();
        $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld');
        $this->assertNotSame(0, $response->toArray(false)['hydra:totalItems'], 'The collection is empty.');
        $this->assertJsonContains([
            'hydra:totalItems' => 35,
            'hydra:view' => [
                '@id' => '/books.jsonld?page=1',
                '@type' => 'hydra:PartialCollectionView',
                'hydra:first' => '/books.jsonld?page=1',
                'hydra:last' => '/books.jsonld?page=2',
                'hydra:next' => '/books.jsonld?page=2',
            ],
        ]);
    }
}

You can also help us improve this guide.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop