spiral/pagination
Spiral Pagination Toolkit provides lightweight, framework-agnostic pagination primitives for PHP apps. Build and pass around page limits/offsets and related metadata cleanly, with strong type safety, tests, and Psalm support.
Install the Package:
composer require spiral/pagination
Ensure your composer.json specifies PHP ^8.1 or higher.
Understand Core Interfaces: The package provides three primary interfaces:
PageInterface: Represents a single page of data (e.g., getItems(), getTotal()).PaginatorInterface: Manages pagination logic (e.g., getPage(), getPerPage()).CursorPaginatorInterface: For cursor-based pagination (e.g., getCursor()).First Use Case: Wrap a Collection Create a simple paginator for an in-memory collection (e.g., API responses or non-DB data):
use Spiral\Pagination\PageInterface;
use Spiral\Pagination\PaginatorInterface;
class SimplePaginator implements PaginatorInterface
{
private array $items;
private int $perPage;
private int $currentPage;
public function __construct(array $items, int $perPage, int $currentPage = 1)
{
$this->items = $items;
$this->perPage = $perPage;
$this->currentPage = $currentPage;
}
public function getPage(): int
{
return $this->currentPage;
}
public function getPerPage(): int
{
return $this->perPage;
}
public function getTotal(): int
{
return count($this->items);
}
public function getItems(): array
{
$start = ($this->currentPage - 1) * $this->perPage;
return array_slice($this->items, $start, $this->perPage);
}
public function getPageCount(): int
{
return ceil($this->getTotal() / $this->perPage);
}
}
Usage:
$data = range(1, 100);
$paginator = new SimplePaginator($data, 10, 1);
$page = $paginator->getItems(); // Returns [1, 2, ..., 10]
Where to Look First:
src/PageInterface.php, src/PaginatorInterface.php, src/CursorPaginatorInterface.php.tests/ for usage examples (though minimal).Use the interfaces to standardize pagination across ORM and raw queries:
use Spiral\Pagination\PaginatorInterface;
class EloquentPaginator implements PaginatorInterface
{
private \Illuminate\Database\Eloquent\Builder $query;
private int $perPage;
private int $currentPage;
public function __construct(\Illuminate\Database\Eloquent\Builder $query, int $perPage, int $currentPage)
{
$this->query = $query;
$this->perPage = $perPage;
$this->currentPage = $currentPage;
}
public function getItems(): array
{
return $this->query->skip(($this->currentPage - 1) * $this->perPage)
->take($this->perPage)
->get()
->toArray();
}
public function getTotal(): int
{
return $this->query->count();
}
// Implement other PaginatorInterface methods...
}
Integration with Laravel Controllers:
public function index(Request $request)
{
$perPage = $request->input('per_page', 15);
$currentPage = $request->input('page', 1);
$query = User::query();
$paginator = new EloquentPaginator($query, $perPage, $currentPage);
return response()->json([
'data' => $paginator->getItems(),
'pagination' => [
'total' => $paginator->getTotal(),
'per_page' => $paginator->getPerPage(),
'current_page' => $paginator->getPage(),
'last_page' => $paginator->getPageCount(),
]
]);
}
Implement CursorPaginatorInterface for APIs needing efficient cursor-based pagination (e.g., GraphQL, infinite scroll):
use Spiral\Pagination\CursorPaginatorInterface;
class CursorPaginator implements CursorPaginatorInterface
{
private array $items;
private string $cursor;
private int $perPage;
public function __construct(array $items, string $cursor, int $perPage)
{
$this->items = $items;
$this->cursor = $cursor;
$this->perPage = $perPage;
}
public function getCursor(): string
{
return $this->cursor;
}
public function getItems(): array
{
// Logic to fetch items after cursor (e.g., from DB)
return array_slice($this->items, 0, $this->perPage);
}
public function getNextCursor(): ?string
{
// Return next cursor or null if no more items
return count($this->items) > $this->perPage ? 'next_cursor_value' : null;
}
}
Create a response DTO to standardize pagination across all API endpoints:
use Spiral\Pagination\PageInterface;
class PaginatedResponse
{
public function __construct(private PageInterface $page)
{
}
public function toArray(): array
{
return [
'data' => $this->page->getItems(),
'meta' => [
'pagination' => [
'total' => $this->page->getTotal(),
'per_page' => $this->page->getPerPage(),
'current_page' => $this->page->getPage(),
'last_page' => $this->page->getPageCount(),
]
]
];
}
}
Usage:
$response = new PaginatedResponse($paginator);
return response()->json($response->toArray());
Decouple pagination logic from controllers by creating a pagination service:
class PaginationService
{
public function paginateCollection(array $collection, int $perPage, int $page = 1): PageInterface
{
return new SimplePaginator($collection, $perPage, $page);
}
public function paginateQuery(\Illuminate\Database\Eloquent\Builder $query, int $perPage, int $page = 1): PageInterface
{
return new EloquentPaginator($query, $perPage, $page);
}
}
Controller Usage:
public function show(Request $request)
{
$service = new PaginationService();
$paginator = $service->paginateQuery(User::query(), 10, $request->page);
return response()->json($paginator->getItems());
}
Leverage Laravel’s Service Container: Bind your paginators as singletons or resolve them dynamically:
$app->bind(PaginationService::class, function ($app) {
return new PaginationService();
});
Compose with Laravel’s Pagination:
Adapt Laravel’s LengthAwarePaginator to PaginatorInterface:
use Spiral\Pagination\PaginatorInterface;
use Illuminate\Pagination\LengthAwarePaginator;
class LaravelPaginatorAdapter implements PaginatorInterface
{
public function __construct(private LengthAwarePaginator $paginator)
{
}
public function getItems(): array
{
return $this->paginator->items();
}
public function getTotal(): int
{
return $this->paginator->total();
}
// Implement other methods...
}
Testing:
Mock PageInterface in unit tests:
$mockPage = $this->createMock(PageInterface::class);
$mockPage->method('getItems')->willReturn([1, 2, 3]);
$mockPage->method('getTotal')->willReturn(100);
Frontend Integration: Return consistent JSON structures for React/Vue:
return response()->json([
'data' => $paginator->getItems(),
'pagination' => [
'total' => $paginator->getTotal(),
'per_page' => $paginator->getPerPage(),
'current_page' => $paginator->getPage(),
'next_cursor' => $paginator instanceof CursorPaginatorInterface ? $paginator->getNextCursor() : null,
]
]);
How can I help you explore Laravel packages today?