Installation:
composer require dwalczyk/paginator-bundle
Register the bundle in config/bundles.php:
DWalczyk\Paginator\PaginatorBundle::class => ['all' => true]
First Use Case:
Inject PaginatorInterface into a controller/service and paginate a Doctrine QueryBuilder:
use DWalczyk\Paginator\PaginatorInterface;
#[Route('/users')]
public function listUsers(PaginatorInterface $paginator)
{
$query = $this->getEntityManager()->createQueryBuilder()
->select('u')
->from(User::class, 'u');
$result = $paginator->paginate($query, 1, 20);
return $this->render('users/index.html.twig', ['users' => $result]);
}
Key Files to Review:
src/PaginatorBundle/Resources/config/services.yaml (default service config).src/DataLoader/DoctrineDataLoader.php (default implementation for QueryBuilder).Doctrine QueryBuilder Integration:
PaginatorInterface::paginate() with a QueryBuilder instance for automatic pagination.$paginator->paginate($qb->getQuery(), $page, $limit);
PaginatorResultInterface with:
getItems(): Array of results for the current page.getTotalCount(): Total items across all pages.getCurrentPage(): Current page number.getItemsPerPage(): Items per page.Custom Data Sources:
DataLoaderInterface for non-Doctrine data (e.g., API responses, CSV files):
class ApiDataLoader implements DataLoaderInterface {
public function loadItems(mixed $apiUrl, int $offset, int $limit): array {
return $this->httpClient->request('GET', $apiUrl . "?offset=$offset&limit=$limit")->toArray();
}
public function loadTotalCount(mixed $apiUrl): int {
return $this->httpClient->request('GET', $apiUrl . '/count')->toArray()['total'];
}
}
config/services.yaml:
services:
DWalczyk\Paginator\DataLoaderInterface: '@App\Service\ApiDataLoader'
Twig Integration:
PaginatorResultInterface to Twig templates for pagination links:
{% for user in result.items %}
{{ user.name }}
{% endfor %}
{% if result.hasPreviousPage %}
<a href="{{ path('users', { page: result.previousPage }) }}">Previous</a>
{% endif %}
Dynamic Limits: Use dependency injection to pass configurable limits:
public function __construct(private int $defaultLimit) {}
$paginator->paginate($query, $page, $this->defaultLimit);
Caching:
Cache loadTotalCount() results if the dataset is static:
public function loadTotalCount(mixed $target): int {
return $this->cache->get('paginator:total:' . spl_object_hash($target), function() use ($target) {
return $this->repository->count([]);
});
}
Symfony Messenger Integration: Defer heavy pagination to a background job:
$this->messageBus->dispatch(new PaginateUsersMessage($query, $page, $limit));
Symfony 7.x Dependency:
Class 'Symfony\Component\HttpFoundation\Request' not found → Add symfony/http-foundation to composer.json.QueryBuilder Modifications:
OFFSET/LIMIT, add them manually:
$qb->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
OFFSET/LIMIT will return all results for the first page.DataLoader Contracts:
loadItems() must return an array of items, not a Collection or ArrayObject.loadTotalCount() must return an integer, not a Countable object.Circular References:
loadItems(). Use DTOs or hydrate entities post-pagination.Verify DataLoader Binding:
DataLoaderInterface is correctly bound:
php bin/console debug:container DWalczyk\Paginator\DataLoaderInterface
php bin/console cache:clear) if changes aren’t reflected.Pagination Off-by-One Errors:
getCurrentPage() returns 0 instead of 1, ensure your DataLoaderInterface aligns with the package’s expectations. Override getCurrentPage() in a custom PaginatorResult if needed.Performance Issues:
COUNT(*) in loadTotalCount(). Use a pre-aggregated count column or estimate:
public function loadTotalCount(mixed $target): int {
return $this->repository->getCountFromCache(); // Hypothetical cached count
}
Custom PaginatorResult:
Extend PaginatorResult to add metadata:
class ExtendedPaginatorResult extends PaginatorResult {
public function hasMorePages(): bool {
return $this->getCurrentPage() * $this->getItemsPerPage() < $this->getTotalCount();
}
}
Bind it in services.yaml:
DWalczyk\Paginator\PaginatorInterface:
arguments:
$resultClass: App\Service\ExtendedPaginatorResult
Event Listeners:
Hook into pagination events (e.g., PaginatorEvent::PRE_LOAD) via Symfony’s event dispatcher:
$dispatcher->addListener(PaginatorEvent::PRE_LOAD, function(PaginatorEvent $event) {
if ($event->getTarget() instanceof UserQuery) {
$event->getQueryBuilder()->andWhere('u.active = 1');
}
});
API Platform Integration:
Use with API Platform’s Collection responses:
#[ApiResource(paginationItemsPerPage: 20)]
class User {}
Override the paginator in config/packages/api_platform.yaml:
api_platform:
pagination:
enabled: true
client_items_per_page: true
items_per_page_parameter_name: 'itemsPerPage'
paginator: '@DWalczyk\Paginator\PaginatorInterface'
How can I help you explore Laravel packages today?