PHP e Cookie: mantenere filtri e ordinamento di una tabella

by theArchitect
SHARE
PHP e Cookie: mantenere filtri e ordinamento di una tabella
© Guida-HTML5.it

Introduzione

Un caso pratico di persistenza con i cookie consiste nel ricordare le impostazioni di una tabella: il campo usato per l’ordinamento, la direzione crescente o decrescente e alcuni filtri di ricerca. In questo modo, quando l’utente torna sulla pagina, ritrova l’elenco configurato come lo aveva lasciato.

Questa soluzione è diversa dal salvataggio di un tema o di una lingua, perché riguarda lo stato di consultazione di dati dinamici. È utile, ad esempio, in un pannello amministrativo, in una lista di ordini o in una pagina contenente prodotti.

Il cookie non deve contenere dati sensibili né l’intero risultato della query. È sufficiente memorizzare pochi valori, come sort, direction e status. Il server leggerà questi dati e li utilizzerà per costruire correttamente la risposta.

Codice completo

Il seguente esempio utilizza un unico file PHP. Il cookie viene salvato in formato JSON e contiene soltanto valori ammessi da una lista prestabilita.

<?php
declare(strict_types=1);

// Valori consentiti per evitare input arbitrari.
$allowedSorts = [´name´, ´created_at´, ´price´];
$allowedDirections = [´asc´, ´desc´];
$allowedStatuses = [´all´, ´active´, ´archived´];

$cookieName = ´table_preferences´;

// Valori predefiniti usati quando il cookie non esiste o non è valido.
$preferences = [
    ´sort´ => ´created_at´,
    ´direction´ => ´desc´,
    ´status´ => ´all´,
];

// Lettura del cookie esistente.
if (isset($_COOKIE[$cookieName])) {
    $decoded = json_decode($_COOKIE[$cookieName], true);

    if (is_array($decoded)) {
        $sort = $decoded[´sort´] ?? null;
        $direction = $decoded[´direction´] ?? null;
        $status = $decoded[´status´] ?? null;

        // Accettiamo solo valori presenti nelle whitelist.
        if (
            in_array($sort, $allowedSorts, true) &&
            in_array($direction, $allowedDirections, true) &&
            in_array($status, $allowedStatuses, true)
        ) {
            $preferences = [
                ´sort´ => $sort,
                ´direction´ => $direction,
                ´status´ => $status,
            ];
        }
    }
}

// Aggiornamento delle preferenze tramite il form.
if ($_SERVER[´REQUEST_METHOD´] === ´POST´) {
    $newSort = $_POST[´sort´] ?? ´´;
    $newDirection = $_POST[´direction´] ?? ´´;
    $newStatus = $_POST[´status´] ?? ´´;

    if (
        in_array($newSort, $allowedSorts, true) &&
        in_array($newDirection, $allowedDirections, true) &&
        in_array($newStatus, $allowedStatuses, true)
    ) {
        $preferences = [
            ´sort´ => $newSort,
            ´direction´ => $newDirection,
            ´status´ => $newStatus,
        ];

        // JSON compatto e leggibile dal server.
        $cookieValue = json_encode($preferences, JSON_THROW_ON_ERROR);

        // setcookie() deve essere chiamata prima di qualsiasi output.
        setcookie($cookieName, $cookieValue, [
            ´expires´ => time() + (60 * 60 * 24 * 30), // 30 giorni
            ´path´ => ´/´,
            ´secure´ => isset($_SERVER[´HTTPS´]),
            ´httponly´ => true,
            ´samesite´ => ´Lax´,
        ]);

        // Evita il reinvio del form aggiornando la pagina.
        header(´Location: ´ . $_SERVER[´PHP_SELF´]);
        exit;
    }
}

// Esempio di query: i valori sono già stati convalidati.
$orderBy = $preferences[´sort´];
$orderDirection = strtoupper($preferences[´direction´]);
$status = $preferences[´status´];

// In un´applicazione reale, usare questi valori con una query PDO.
// Esempio concettuale:
// SELECT * FROM products
// WHERE status = :status
// ORDER BY created_at DESC
?>

<form method="post">
    <label>
        Ordina per:
        <select name="sort">
            <option value="name">Nome</option>
            <option value="created_at">Data di creazione</option>
            <option value="price">Prezzo</option>
        </select>
    </label>

    <label>
        Direzione:
        <select name="direction">
            <option value="asc">Crescente</option>
            <option value="desc">Decrescente</option>
        </select>
    </label>

    <label>
        Stato:
        <select name="status">
            <option value="all">Tutti</option>
            <option value="active">Attivi</option>
            <option value="archived">Archiviati</option>
        </select>
    </label>

    <button type="submit">Salva preferenze</button>
</form>

<p>
    Ordinamento attuale:
    <strong><?= htmlspecialchars($orderBy, ENT_QUOTES, ´UTF-8´) ?></strong>,
    direzione:
    <strong><?= htmlspecialchars($orderDirection, ENT_QUOTES, ´UTF-8´) ?></strong>,
    stato:
    <strong><?= htmlspecialchars($status, ENT_QUOTES, ´UTF-8´) ?></strong>.
</p>

Spiegazione

Struttura del cookie

Il valore del cookie è un oggetto JSON, ad esempio:

{"sort":"price","direction":"asc","status":"active"}

JSON è più ordinato e facilmente estendibile rispetto a una stringa composta manualmente con separatori. In futuro si potrebbe aggiungere un valore come pageSize, cioè il numero di righe visualizzate per pagina.

Validazione dei dati

Un cookie può essere modificato direttamente dall’utente tramite gli strumenti del browser. Per questo motivo non bisogna mai fidarsi del suo contenuto. Le whitelist verificano che ogni valore appartenga all’elenco previsto.

Questa protezione è particolarmente importante quando i dati vengono usati in una query SQL. Il nome della colonna per l’ordinamento non dovrebbe essere inserito direttamente dalla richiesta: deve essere scelto da una mappa controllata dal server.

$columns = [
    ´name´ => ´p.name´,
    ´created_at´ => ´p.created_at´,
    ´price´ => ´p.price´,
];

$orderBySql = $columns[$preferences[´sort´]];

In questo modo l’utente invia solo una chiave conosciuta, mentre il server decide quale colonna SQL utilizzare.

Perché usare il redirect

Dopo il salvataggio viene eseguito un redirect. Questo schema, chiamato Post/Redirect/Get, evita che un aggiornamento del browser invii nuovamente il form e registri più volte la stessa operazione.

Best practice

  • Salvare pochi dati: i cookie hanno una dimensione limitata, generalmente circa 4 KB per cookie.
  • Non memorizzare dati sensibili: filtri e ordinamento vanno bene; password, token segreti e dati personali no.
  • Usare una whitelist: ogni valore deve essere confrontato con opzioni note.
  • Impostare una scadenza: trenta giorni sono spesso sufficienti per preferenze di consultazione.
  • Usare HttpOnly: impedisce agli script JavaScript di leggere il cookie, quando non serve accedervi dal client.
  • Usare Secure in HTTPS: il cookie viene trasmesso solo tramite connessioni cifrate.
  • Impostare SameSite: il valore Lax offre una protezione ragionevole contro molte richieste cross-site.
  • Escapare l’output: anche se il dato proviene da un cookie, va filtrato prima di essere stampato nell’HTML.
  • Separare il valore logico dal valore SQL: il cookie non deve decidere direttamente quale testo inserire nella query.

Se le preferenze devono essere sincronizzate tra più dispositivi, il cookie non è più sufficiente. In quel caso conviene salvare i dati nel database associandoli all’account dell’utente, usando il cookie soltanto per utenti anonimi.

Riepilogo

I cookie sono adatti a mantenere piccole preferenze locali, come filtri, ordinamento e stato di visualizzazione di una tabella. Una soluzione robusta deve leggere il cookie in modo prudente, verificare ogni valore, impostare una scadenza e proteggere i parametri usati nelle query.

Il cookie non è un archivio sicuro: è un dato controllato dal browser dell’utente. La regola fondamentale è quindi semplice: usarlo per migliorare l’esperienza, ma non per prendere decisioni di sicurezza.

Approfondisci con risorse ufficiali

SHARE