PHP e PDO: creare una paginazione efficiente dei risultati

by theArchitect
SHARE
PHP e PDO: creare una paginazione efficiente dei risultati
© Guida-HTML5.it

Introduzione

Quando un’applicazione deve mostrare molti record provenienti da un database, caricarli tutti in una sola pagina non è una buona soluzione. Un elenco di migliaia di prodotti, articoli o utenti può rallentare la query, aumentare il consumo di memoria e rendere l’interfaccia difficile da usare.

La paginazione permette di suddividere i risultati in pagine più piccole. Per esempio, possiamo visualizzare 10 prodotti alla volta e offrire all’utente i collegamenti alle pagine successive. In PHP, PDO consente di eseguire questa operazione in modo sicuro usando query preparate e parametri associati.

In questo tutorial realizzeremo un esempio completo con una tabella products. Utilizzeremo la clausola SQL LIMIT per stabilire quanti record leggere e OFFSET per indicare da quale record iniziare.

Codice completo

Supponiamo di avere una tabella con questa struttura:

CREATE TABLE products (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(150) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Il seguente file PHP apre una connessione al database, calcola la pagina richiesta, recupera i prodotti e genera i collegamenti per navigare tra le pagine.

<?php

declare(strict_types=1);

// Parametri di connessione
$host = ´localhost´;
$dbname = ´catalogo´;
$username = ´app_user´;
$password = ´password_sicura´;

$dsn = "mysql:host={$host};dbname={$dbname};charset=utf8mb4";

$pdo = new PDO($dsn, $username, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

// Numero massimo di prodotti visualizzati per pagina
$perPage = 10;

// La pagina arriva dall´URL, per esempio: products.php?page=2
$page = filter_input(
    INPUT_GET,
    ´page´,
    FILTER_VALIDATE_INT,
    [
        ´options´ => [
            ´default´ => 1,
            ´min_range´ => 1,
        ],
    ]
);

// Calcolo dell´indice iniziale
$offset = ($page - 1) * $perPage;

// Conteggio totale dei prodotti
$countStatement = $pdo->query(
    ´SELECT COUNT(*) FROM products´
);

$totalProducts = (int) $countStatement->fetchColumn();

// Calcolo del numero totale di pagine
$totalPages = max(1, (int) ceil($totalProducts / $perPage));

// Se la pagina richiesta supera l´ultima, torniamo all´ultima pagina
$page = min($page, $totalPages);
$offset = ($page - 1) * $perPage;

// Recupero dei prodotti della pagina corrente
$sql = ´
    SELECT id, name, price, created_at
    FROM products
    ORDER BY created_at DESC, id DESC
    LIMIT :limit OFFSET :offset
´;

$statement = $pdo->prepare($sql);

// LIMIT e OFFSET rappresentano numeri interi
$statement->bindValue(´:limit´, $perPage, PDO::PARAM_INT);
$statement->bindValue(´:offset´, $offset, PDO::PARAM_INT);

$statement->execute();

$products = $statement->fetchAll();

?>

<h1>Catalogo prodotti</h1>

<ul>
    <?php foreach ($products as $product): ?>
        <li>
            <strong>
                <?= htmlspecialchars($product[´name´], ENT_QUOTES, ´UTF-8´) ?>
            </strong>

            -
            <?= number_format((float) $product[´price´], 2, ´,´, ´.´) ?> €
        </li>
    <?php endforeach; ?>
</ul>

<nav aria-label="Paginazione prodotti">
    <?php if ($page > 1): ?>
        <a href="?page=<?= $page - 1 ?>">Pagina precedente</a>
    <?php endif; ?>

    <span>
        Pagina <?= $page ?> di <?= $totalPages ?>
    </span>

    <?php if ($page < $totalPages): ?>
        <a href="?page=<?= $page + 1 ?>">Pagina successiva</a>
    <?php endif; ?>
</nav>

Spiegazione

Validare il numero di pagina

Il valore page viene normalmente ricevuto tramite la query string dell’URL. Un utente potrebbe però inserire valori non validi, come page=abc, page=0 o un numero negativo.

La funzione filter_input() consente di convertire il valore in un intero e applicare un limite minimo. In questo modo la prima pagina valida è sempre la pagina 1.

Calcolare OFFSET

La formula utilizzata è:

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

Se mostriamo 10 record per pagina, la prima pagina usa OFFSET 0, la seconda usa OFFSET 10, la terza usa OFFSET 20 e così via.

Contare i record

La query SELECT COUNT(*) restituisce il numero complessivo di prodotti. Questo valore serve per calcolare quante pagine devono essere mostrate:

$totalPages = (int) ceil($totalProducts / $perPage);

La funzione ceil() arrotonda per eccesso. Se ci sono 25 prodotti e 10 elementi per pagina, il risultato sarà 3 pagine.

Associare correttamente i parametri

È importante associare LIMIT e OFFSET come interi usando PDO::PARAM_INT. In assenza di questa indicazione, alcuni driver potrebbero trattare i valori come stringhe e generare errori SQL o comportamenti imprevisti.

La clausola ORDER BY è altrettanto importante. Senza un ordinamento stabile, il database potrebbe restituire i record in ordine diverso tra una richiesta e l’altra. L’ordinamento per created_at e id rende il risultato più prevedibile, anche quando due prodotti hanno la stessa data.

Proteggere l’output HTML

I dati letti dal database non devono essere inseriti direttamente nel codice HTML. Un nome prodotto potrebbe contenere caratteri speciali o, se i dati non sono stati validati correttamente, codice potenzialmente pericoloso.

Per questo viene usata htmlspecialchars(), che converte i caratteri speciali in entità HTML. La protezione contro SQL Injection e la protezione dell’output HTML sono problemi differenti: PDO protegge le query, mentre htmlspecialchars() protegge la visualizzazione nella pagina.

Best practice

  • Imposta un limite ragionevole: valori come 10, 20 o 50 record per pagina sono generalmente più gestibili di centinaia di righe.
  • Usa sempre un ordinamento: una paginazione senza ORDER BY può produrre risultati instabili.
  • Associa i numeri come interi: usa PDO::PARAM_INT per LIMIT e OFFSET.
  • Controlla la pagina richiesta: non fidarti mai direttamente dei parametri ricevuti dall’URL.
  • Gestisci la pagina oltre il limite: riportare il valore all’ultima pagina evita una query senza risultati quando l’utente inserisce un numero troppo alto.
  • Aggiungi indici utili: se ordini frequentemente per created_at, valuta un indice su quella colonna.
  • Proteggi l’output: usa htmlspecialchars() quando mostri testo proveniente dal database.
  • Considera la paginazione a cursore: per tabelle enormi, LIMIT/OFFSET può diventare lento con offset molto elevati. In questi casi è preferibile caricare i record successivi usando l’ultimo id visualizzato.

Un esempio di indice utile può essere:

CREATE INDEX idx_products_created_id
ON products (created_at, id);

La scelta dell’indice deve comunque essere verificata analizzando il piano di esecuzione della query e le dimensioni reali della tabella.

Riepilogo

La paginazione con PHP e PDO consente di visualizzare grandi quantità di dati in modo ordinato e sostenibile. Il procedimento principale consiste nel validare la pagina, calcolare l’offset, contare i record totali, eseguire una query con LIMIT e OFFSET e infine creare i collegamenti di navigazione.

Le parti più importanti sono la validazione dei parametri, l’uso di query preparate, il binding esplicito degli interi e un ordinamento stabile. Applicando anche l’escaping dell’output e gli indici appropriati, si ottiene una soluzione più sicura e performante.

Approfondisci con risorse ufficiali

SHARE