Paginando en symfony
La paginación de resultados es un requisito común en proyectos web, pero su implementación puede volverse compleja rápidamente. Aunque Symfony cuenta con bundles como knp-paginator-bundle, en ocasiones necesitamos una solución más personalizada. Te presento una implementación que extiende el paginador de Doctrine, ofreciendo metadatos enriquecidos para su uso en vistas y APIs.
<?php
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
use ArrayIterator;
class EnhancedPaginator extends DoctrinePaginator
{
public const DEFAULT_ITEMS_PER_PAGE = 9;
private int $totalItems;
private array $currentPageItems;
private int $nbPages;
private int $currentPage;
public function __construct(
QueryBuilder|Query $query,
int $page = 1,
int $itemsPerPage = self::DEFAULT_ITEMS_PER_PAGE,
bool $fetchJoinCollection = true
) {
$this->currentPage = max(1, $page);
$offset = ($this->currentPage - 1) * $itemsPerPage;
$query->setFirstResult($offset)
->setMaxResults($itemsPerPage);
parent::__construct($query, $fetchJoinCollection);
$this->totalItems = parent::count();
$this->currentPageItems = iterator_to_array(parent::getIterator());
$this->nbPages = $this->calculateTotalPages($itemsPerPage);
}
private function calculateTotalPages(int $itemsPerPage): int
{
if ($itemsPerPage < 1) return 0;
return (int) ceil($this->totalItems / $itemsPerPage);
}
// Métodos de acceso a la información de paginación
public function getTotalItems(): int
{
return $this->totalItems;
}
public function getItemsPerPage(): int
{
return $this->getQuery()->getMaxResults() ?? self::DEFAULT_ITEMS_PER_PAGE;
}
public function getCurrentPage(): int
{
return $this->currentPage;
}
public function getTotalPages(): int
{
return $this->nbPages;
}
public function hasNextPage(): bool
{
return $this->currentPage < $this->nbPages;
}
public function hasPreviousPage(): bool
{
return $this->currentPage > 1;
}
public function getResults(): array
{
return $this->currentPageItems;
}
public function getPaginationData(): array
{
return [
'results' => $this->getResults(),
'pagination' => [
'total_items' => $this->getTotalItems(),
'items_per_page' => $this->getItemsPerPage(),
'current_page' => $this->getCurrentPage(),
'total_pages' => $this->getTotalPages(),
'has_previous_page' => $this->hasPreviousPage(),
'has_next_page' => $this->hasNextPage(),
'previous_page' => $this->hasPreviousPage() ? $this->currentPage - 1 : null,
'next_page' => $this->hasNextPage() ? $this->currentPage + 1 : null,
]
];
}
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->getPaginationData());
}
}Principales Mejoras y Características
Configuración Flexible de Items por Página
// Uso básico $paginator = new EnhancedPaginator($query, $page); // Personalización $paginator = new EnhancedPaginator($query, $page, 15);
Lógica de Paginación Optimizada
- Cálculo seguro de offsets
- Validación automática de números de página
- Métodos simplificados para navegación
Metadatos Enriquecidos
{
"results": [...],
"pagination": {
"total_items": 45,
"items_per_page": 9,
"current_page": 2,
"total_pages": 5,
"has_previous_page": true,
"has_next_page": true,
"previous_page": 1,
"next_page": 3
}
}Manejo de Errores Mejorado
- Protección contra valores negativos en páginas
- Cálculo seguro de total de páginas
Cómo Usarlo en tu Proyecto
En tu repositorio o servicio:
public function findPaginated(int $page, int $itemsPerPage = 10): EnhancedPaginator
{
$qb = $this->createQueryBuilder('p')
->orderBy('p.createdAt', 'DESC');
return new EnhancedPaginator($qb->getQuery(), $page, $itemsPerPage);
}En tu controlador:
public function listProducts(Request $request): JsonResponse
{
$page = $request->query->getInt('page', 1);
$paginator = $this->productRepository->findPaginated($page);
return $this->json($paginator->getPaginationData());
}En tu plantilla Twig:
{% for product in paginator.results %}
{{ product.name }}
{% endfor %}
<nav>
{% if paginator.pagination.has_previous_page %}
<a href="?page={{ paginator.pagination.previous_page }}">Anterior</a>
{% endif %}
Página {{ paginator.pagination.current_page }} de {{ paginator.pagination.total_pages }}
{% if paginator.pagination.has_next_page %}
<a href="?page={{ paginator.pagination.next_page }}">Siguiente</a>
{% endif %}
</nav>Ventajas Clave
Encapsulamiento Completo
- Toda la lógica de paginación está contenida en la clase
- Fácil de testear y reutilizar
Interfaz Intuitiva
- Métodos claramente nombrados
- Estructura de datos consistente
Extensible
- Fácil de añadir nuevos métodos (ej. saltar a página específica)
- Adaptable a diferentes necesidades de paginación
Posibles Mejoras Futuras
- Implementar caché para conteos de resultados
- Añadir soporte para diferentes estrategias de paginación
- Integrar validación avanzada de parámetros
- Agregar métodos para rangos de páginas
Este enfoque proporciona una solución robusta y mantenible para la paginación, especialmente útil cuando necesitamos información detallada para construir interfaces de usuario complejas o APIs RESTful bien documentadas.
Deja un comentario: