Paginando en symfony

Paginando en symfony

  • Ruben
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.


  • Guías prácticas
  • Symfony
  • PHP
Deja un comentario: