The bundle provides built-in support for paginated collection endpoints with a standardized response format.
namespace OpenSolid\Core\Domain\Repository;
/**
* @template T of object
*/
interface Paginator extends \Traversable, \Countable
{
public function getCurrentPage(): int;
public function getItemsPerPage(): int;
public function getLastPage(): int;
public function getTotalItems(): int;
}A concrete implementation that works with Doctrine's Selectable interface (e.g. Doctrine Collections, Repositories):
use OpenSolid\Core\Domain\Repository\SelectablePaginator;
$paginator = new SelectablePaginator(
selectable: $repository, // Any Selectable & Countable
currentPage: $query->page,
itemsPerPage: $query->itemsPerPage,
);It supports an optional mapper to transform entities into view objects:
$paginator = new SelectablePaginator(
selectable: $repository,
currentPage: $query->page,
itemsPerPage: $query->itemsPerPage,
mapper: fn(Product $product) => new ProductView($product),
);Use #[GetCollection] with a Paginator return type:
use OpenSolid\Api\Routing\Attribute\GetCollection;
use OpenSolid\Core\Domain\Repository\Paginator;
use OpenSolid\Core\Domain\Repository\SelectablePaginator;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
#[GetCollection(
path: '/products',
name: 'api_find_products',
description: 'Find Products',
tags: ['Product'],
)]
final readonly class FindProductsController
{
/**
* @return Paginator<ProductView>
*/
public function __invoke(#[MapQueryString] ?FindProductsQuery $query = null): Paginator
{
$query ??= new FindProductsQuery();
return new SelectablePaginator(
selectable: $this->repository,
currentPage: $query->page,
itemsPerPage: $query->itemsPerPage,
);
}
}The PaginationParams trait provides standard page and itemsPerPage query parameters with OpenAPI annotations:
use OpenApi\Attributes as OA;
use OpenSolid\Api\Controller\Model\Paginator\PaginationParams;
final class FindProductsQuery
{
use PaginationParams; // Adds page (default: 1) and itemsPerPage (default: 20)
#[OA\QueryParameter(description: 'Filter by product name')]
public ?string $name = null;
}The trait defines:
| Parameter | Type | Default | Constraints |
|---|---|---|---|
page |
integer | 1 | minimum: 1 |
itemsPerPage |
integer | 20 | minimum: 1, maximum: 100 |
When a controller returns a Paginator and the route has pagination enabled, the ApiPaginationDecorator wraps the result in a PageResponse:
{
"items": [
{ "id": "...", "name": "Product A", "price": { "amount": 100, "currency": "USD" } },
{ "id": "...", "name": "Product B", "price": { "amount": 200, "currency": "EUR" } }
],
"totalItems": 42
}The AugmentOperations processor detects the Paginator<T> return type and generates a paginated schema:
responses:
'200':
content:
application/json:
schema:
type: object
properties:
items:
type: array
description: The list of items
items:
$ref: '#/components/schemas/ProductView'
totalItems:
type: integer
description: The total number of items
example: 1This schema is generated by PaginatorSchema and requires:
- The return type is
Paginator<T>(with a@returndocblock) - The
_api_paginationroute default istrue(set by#[GetCollection])