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 BYpuò produrre risultati instabili. - Associa i numeri come interi: usa
PDO::PARAM_INTperLIMITeOFFSET. - 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/OFFSETpuò diventare lento con offset molto elevati. In questi casi è preferibile caricare i record successivi usando l’ultimoidvisualizzato.
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.
