Retornar

Paginação sem bundles Symfony

Publicado em 2025-05-30 por Higor Zica

Capa

Exemplo completo de como realizar uma paginação no Symfony sem utilisar uma biblioteca (bundle) :

CONTROLLER :

public function __invoke(Request $request): Response
{
    $page = max(1, (int) $request->query->get('page', 1));
    $limit = max(1, (int) $request->query->get('limit', 2));

    $result = $this->productRepository->fetchAll($page, $limit);
    $products = $result['products'];
    $total = $result['total'];

    return new Response(
        $this->twig->render(
            'product/list.html.twig',
            [
                'products' => $products,
                'page' => $page,
                'limit' => $limit,
                'total' => $total,
            ]
        )
    );
}

🔍 Passo a passo

1. Função __invoke(Request $request)

  • Esse método mágico __invoke() permite que a classe do controller seja usada como uma função.

  • Isso é comum quando usamos o controller como um "action" direto em rotas Symfony:

    product_list:
      path: /products
      controller: App\Controller\ProductListController
    

2. Recuperando os parâmetros da URL

$page = max(1, (int) $request->query->get('page', 1));
$limit = max(1, (int) $request->query->get('limit', 2));
  • Request $request: o Symfony injeta automaticamente a requisição HTTP.
  • query->get('page', 1): busca o parâmetro page na URL (/products?page=2), e se não tiver, usa 1 por padrão.
  • max(1, ...): garante que não venha 0 ou valores negativos (evita bugs).

Exemplo:

  • Se a URL for /products?page=3&limit=4, então:
    • $page = 3
    • $limit = 4

3. Busca os produtos paginados no repositório

$result = $this->productRepository->fetchAll($page, $limit);
  • Chama o método que vimos antes no repositório.
  • fetchAll($page, $limit) retorna um array com:
    • 'products' => [...]
    • 'total' => 5 (por exemplo)

4. Extrai os dados do resultado

$products = $result['products'];
$total = $result['total'];

Só está quebrando o array $result em variáveis separadas, pra facilitar o uso na view.


5. Renderiza o template Twig

return new Response(
    $this->twig->render(
        'product/list.html.twig',
        [
            'products' => $products,
            'page' => $page,
            'limit' => $limit,
            'total' => $total,
        ]
    )
);
  • Gera o HTML com o Twig (list.html.twig) e passa variáveis:
    • 'products' → lista dos produtos da página
    • 'page' → número da página atual
    • 'limit' → limite de produtos por página
    • 'total' → total de produtos existentes
  • Depois cria e retorna um Response com o HTML gerado.

💡 Exemplo real

Se você acessar:
/products?page=2&limit=2

Então:

  • $page = 2
  • $limit = 2
  • O repositório retorna os produtos de índice 2 e 3
  • O template Twig renderiza só esses produtos e mostra a paginação corretamente.

REPOSITORY :

public function fetchAll(int $page = 1, int $limit = 2): array
{
    $allProducts = [
        new ListProduct(id: 1, sku: 'PATA03731', name: 'M\'s Torrentshell 3L Rain Jkt Endless Blue', price: 199.9, promoPrice: 129.9, brand: 'Patagonia'),
        new ListProduct(id: 2, sku: 'NIKA01001', name: 'Nike Air Max Alpha Trainer', price: 149.9, promoPrice: 99.9, brand: 'Nike'),
        new ListProduct(id: 3, sku: 'ADID02022', name: 'Adidas Ultraboost Light Running', price: 179.9, promoPrice: 139.9, brand: 'Adidas'),
        new ListProduct(id: 4, sku: 'NORT33333', name: 'The North Face Down Jacket', price: 249.9, promoPrice: 199.9, brand: 'The North Face'),
        new ListProduct(id: 5, sku: 'COLUMB7777', name: 'Columbia Waterproof Hiking Boots', price: 159.9, promoPrice: 109.9, brand: 'Columbia'),
        // ... pode adicionar mais produtos depois
    ];

    $offset = ($page - 1) * $limit;

    return [
        'products' => array_slice($allProducts, $offset, $limit),
        'total' => count($allProducts),
    ];
}

🔍 Passo a passo

1. Definição da função

public function fetchAll(int $page = 1, int $limit = 2): array
  • fetchAll é pública (pode ser acessada de fora da classe).
  • Aceita dois parâmetros opcionais:
    • $page: a página atual (padrão 1)
    • $limit: quantos produtos por página (padrão 2)
  • Retorna um array associativo.

2. Criação da lista de produtos

$allProducts = [ ... ];
  • Aqui você simula um "banco de dados" com uma lista de produtos usando a classe ListProduct.
  • Cada produto tem propriedades como: id, sku, name, price, promoPrice, brand.

3. Cálculo do offset

$offset = ($page - 1) * $limit;

Esse cálculo é essencial para paginação:

  • A ideia é "pular" os produtos das páginas anteriores.
  • Exemplo:
    • Página 1, limite 2 → offset = (1 - 1) * 2 = 0 → mostra produtos [0, 1]
    • Página 2, limite 2 → offset = (2 - 1) * 2 = 2 → mostra produtos [2, 3]
    • Página 3, limite 2 → offset = 4 → mostra produtos [4, 5] (e assim por diante)

4. Retorno da função

return [
    'products' => array_slice($allProducts, $offset, $limit),
    'total' => count($allProducts),
];
  • array_slice($allProducts, $offset, $limit):

    • Corta o array original a partir do offset e pega no máximo $limit produtos.
    • Exemplo: página 2 com limite 2 → array_slice($allProducts, 2, 2) retorna os itens de índice 2 e 3.
  • 'total' => count($allProducts):

    • Retorna o número total de produtos (sem considerar a paginação).
    • Isso é útil no front-end para calcular o número total de páginas, por exemplo.

💡 Exemplo prático

Se você chamar:

$repo->fetchAll(2, 2);

Você recebe:

[
  'products' => [
    produto de índice 2,
    produto de índice 3
  ],
  'total' => 5
]

Se quiser, posso te mostrar como montar uma resposta paginada na API, tipo:

{
  "products": [ ... ],
  "pagination": {
    "page": 2,
    "limit": 2,
    "total": 5
  }
}

VIEW :

<div class="card-footer">
    <!-- Pagination -->
    {% set totalPages = (total / limit)|round(0, 'ceil') %}

    <nav aria-label="Pagination">
        <ul class="pagination">
            {% for i in 1..totalPages %}
                <li class="page-item {% if i == page %}active{% endif %}">
                    <a class="page-link" href="?page={{ i }}&limit={{ limit }}">{{ i }}</a>
                </li>
            {% endfor %}
            <li class="mt-2">
                <span style="margin-left: 10px; font-size: 12px;">Total produits : {{total}}</span>
            </li>
        </ul>
    </nav>
</div>

🧠 Passo a passo

1. Cálculo do número total de páginas

{% set totalPages = (total / limit)|round(0, 'ceil') %}
  • total: número total de produtos (ex: 12)
  • limit: quantos produtos por página (ex: 5)
  • Faz a divisão e usa o |round(0, 'ceil') para arredondar pra cima.
    • Exemplo: 12 / 5 = 2.4 → arredondado pra 3 páginas
  • totalPages agora vale 3

2. Geração da paginação com loop

{% for i in 1..totalPages %}
  • Gera um loop de 1 até totalPages
  • Se totalPages = 3, então ele vai gerar 3 <li> com links.

3. Renderização de cada botão de página

<li class="page-item {% if i == page %}active{% endif %}">
    <a class="page-link" href="?page={{ i }}&limit={{ limit }}">{{ i }}</a>
</li>
  • Se a página atual (page) for igual ao número (i), adiciona a classe active (para destacar a página atual)
  • O link aponta para ?page=i&limit=limit
    • Ex: ?page=2&limit=5

4. Exibe total de produtos

<li class="mt-2">
    <span style="margin-left: 10px; font-size: 12px;">Total produits : {{total}}</span>
</li>
  • Mostra uma mensagem simples: Total produits : 12
  • Não é um item clicável, só informativo.

✅ Resultado final

Imagina que temos 12 produtos, limit = 5. Então o layout seria:

[1] [2] [3]   Total produits: 12

Se você estiver na página 2, ele mostra o botão 2 como "ativo".


Tags:

php symfony