PHPackages                             e2k/cursor-pagination-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. e2k/cursor-pagination-bundle

ActiveSymfony-bundle[Utility &amp; Helpers](/categories/utility)

e2k/cursor-pagination-bundle
============================

Symfony bundle for cursor-based (keyset) pagination with rich filter expression DSL

1.0.0(1mo ago)00MITPHPPHP &gt;=8.1

Since Apr 29Pushed 3w agoCompare

[ Source](https://github.com/ernestkouassi/cursor-pagination-bundle)[ Packagist](https://packagist.org/packages/e2k/cursor-pagination-bundle)[ RSS](/packages/e2k-cursor-pagination-bundle/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (5)Versions (3)Used By (0)

e2k/cursor-pagination-bundle
============================

[](#e2kcursor-pagination-bundle)

Symfony bundle for cursor-based (keyset) pagination with a rich filter expression DSL.

Cursor pagination is O(1) regardless of page depth — unlike OFFSET pagination which degrades linearly.

---

Installation
------------

[](#installation)

```
composer require e2k/cursor-pagination-bundle
```

Register the bundle in `config/bundles.php`:

```
return [
    // ...
    E2k\CursorPaginationBundle\CursorPaginationBundle::class => ['all' => true],
];
```

---

Quick Start
-----------

[](#quick-start)

### 1. Configure the query in your repository

[](#1-configure-the-query-in-your-repository)

```
use E2k\CursorPaginationBundle\CursorFieldDefinition;
use E2k\CursorPaginationBundle\FieldDefinition;
use E2k\CursorPaginationBundle\Pagination\CursorQueryFactory;
use E2k\CursorPaginationBundle\Pagination\CursorResult;

class InvoiceRepository extends ServiceEntityRepository
{
    public function __construct(
        ManagerRegistry $registry,
        private readonly CursorQueryFactory $cursorQueryFactory,
    ) {
        parent::__construct($registry, Invoice::class);
    }

    public function findPageByCursor(array $queryParams, int $limit): CursorResult
    {
        return $this->cursorQueryFactory
            ->create(Invoice::class, 'i')
            ->addCursorField(new CursorFieldDefinition('createdAt', 'i.createdAt', 'datetime'))
            ->addCursorField(new CursorFieldDefinition('id', 'i.id', 'string'))
            ->addFilterableField(new FieldDefinition('status',         'i.status.value'))
            ->addFilterableField(new FieldDefinition('organizationId', 'i.organization'))
            ->addFilterableField(new FieldDefinition('amount',         'i.amount', 'float'))
            ->addFilterableField(new FieldDefinition('reference',      'i.reference'))
            ->execute($queryParams, $limit);
    }
}
```

### 2. Use the result in your controller

[](#2-use-the-result-in-your-controller)

```
public function list(Request $request, InvoiceRepository $repository): JsonResponse
{
    $limit  = max(1, min(100, (int) $request->query->get('itemPerPage', 20)));
    $result = $repository->findPageByCursor($request->query->all(), $limit);

    $items = $this->normalizer->normalize(
        $result->items,
        null,
        ['groups' => ['invoice:read']],
    );

    return $this->json($result->toResponseArray($items));
}
```

### 3. Response format

[](#3-response-format)

```
{
    "itemPerPage": 20,
    "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI0LTAxLTAxVDAwOjAwOjAwLjAwMFoiLCJpZCI6IjEyMyJ9",
    "hasMore": true,
    "filters": {
        "status": "DRAFT",
        "sort": "createdAt",
        "desc": "createdAt"
    },
    "items": [...]
}
```

> **Navigation arrière** : cette API ne fournit pas de `previousCursor`. Pour naviguer en arrière, le client maintient un stack de curseurs côté frontend :
>
> ```
> const stack = [];
> // page suivante : stack.push(currentCursor); navigate(nextCursor)
> // page précédente : navigate(stack.pop() ?? null)
> ```

---

HTTP API Reference
------------------

[](#http-api-reference)

### Pagination parameters

[](#pagination-parameters)

ParameterDescriptionExample`itemPerPage`Items per page (handled by your controller)`?itemPerPage=20``cursor`Opaque cursor from previous response`?cursor=eyJ...`### Sorting (oka\_pagination-compatible)

[](#sorting-oka_pagination-compatible)

ParametersResult`sort=createdAt&desc=createdAt``ORDER BY createdAt DESC``sort=createdAt&asc=createdAt``ORDER BY createdAt ASC``sort=createdAt&sort=amount&desc=createdAt&asc=amount``ORDER BY createdAt DESC, amount ASC`When no direction is specified for a sort field, `ASC` is used by default.

### Filter expressions

[](#filter-expressions)

Any field declared with `addFilterableField()` accepts the following expressions as its query param value:

ExpressionSQL generatedExample`value``field = 'value'``?status=DRAFT``neq(value)``field != 'value'``?status=neq(DRAFT)``like(value)``field LIKE '%value%'``?name=like(acme)``like(value%)``field LIKE 'value%'``?name=like(acme%)``like(%value)``field LIKE '%value'``?name=like(%acme)``like(%value%)``field LIKE '%value%'``?name=like(%acme%)``in(a,b,c)``field IN ('a','b','c')``?status=in(DRAFT,SENT)``gt(value)``field > value``?amount=gt(100)``gte(value)``field >= value``?amount=gte(100)``lt(value)``field < value``?amount=lt(500)``lte(value)``field
