PHP

Un endpoint REST di ricerca in PHP con PDO: routing, filtri e paginazione

calendar_today personTeam EGSOFT schedule4 min di lettura
info

Disclaimer: le informazioni pubblicate in questa sezione hanno finalità di pura divulgazione tecnica. EG Software S.r.l. non si assume alcuna responsabilità per un utilizzo improprio dei contenuti, né per eventuali danni diretti o indiretti derivanti dalla loro applicazione, e non garantisce l'aggiornamento, l'accuratezza o la completezza delle informazioni riportate. Prima di utilizzare in produzione codice o procedure qui descritte, verificane sempre l'adeguatezza al proprio contesto.

Quando un'anagrafica — comuni, prodotti, clienti — deve essere consultata da un'applicazione esterna, come un sito, un'app mobile o un gestionale desktop, la soluzione più diretta è esporre un piccolo endpoint REST in PHP puro, senza per forza ricorrere a un framework. Vediamo come strutturare un endpoint di ricerca con autenticazione a token, routing basato sul path e paginazione sicura.

Autenticare le richieste con un token semplice

Non tutte le API hanno bisogno della complessità di OAuth2 o di un JWT: per un'integrazione interna tra sistemi di fiducia, un token statico condiviso, verificato con hash_equals() per evitare timing attack, è spesso sufficiente. Il token va sempre letto da una variabile d'ambiente, mai scritto nel codice, e confrontato in modo costante nel tempo indipendentemente dalla lunghezza della stringa ricevuta.

Instradare le richieste in base al path

Un endpoint REST minimale non richiede necessariamente un router esterno: è sufficiente leggere REQUEST_URI, estrarne il path con parse_url() e usare l'ultimo segmento come nome dell'operazione richiesta in uno switch. Questo approccio funziona bene per API con un numero limitato di endpoint, mantenendo il codice leggibile senza dipendenze aggiuntive.

<?php

declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

function verificaToken(): void
{
    $headers = getallheaders();
    $token = null;

    if (isset($headers['Authorization']) && preg_match('/Bearer\s+(.*)$/i', $headers['Authorization'], $m)) {
        $token = $m[1];
    }

    if ($token === null || !hash_equals($_ENV['API_TOKEN'], $token)) {
        http_response_code(401);
        echo json_encode(['error' => 'Token mancante o non valido']);
        exit;
    }
}

function cercaPerCitta(PDO $pdo, string $citta): array
{
    $stmt = $pdo->prepare(
        'SELECT id, provincia, istat, citta, cap, regione
         FROM comuni
         WHERE citta LIKE :citta
         ORDER BY citta'
    );
    $stmt->execute([':citta' => '%' . $citta . '%']);

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function elencoPaginato(PDO $pdo, int $pagina, int $perPagina): array
{
    $offset = ($pagina - 1) * $perPagina;

    $stmt = $pdo->prepare(
        'SELECT id, provincia, citta, cap, regione
         FROM comuni
         ORDER BY citta
         LIMIT :limite OFFSET :offset'
    );
    $stmt->bindValue(':limite', $perPagina, PDO::PARAM_INT);
    $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
    $stmt->execute();

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

verificaToken();

$percorso = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segmenti = explode('/', trim((string) $percorso, '/'));
$endpoint = end($segmenti);

try {
    switch ($endpoint) {
        case 'citta':
            if (!isset($_GET['q'])) {
                http_response_code(400);
                echo json_encode(['error' => 'Il parametro q è obbligatorio']);
                exit;
            }
            $risultati = cercaPerCitta($pdo, (string) $_GET['q']);
            echo json_encode(['success' => true, 'count' => count($risultati), 'data' => $risultati]);
            break;

        case 'elenco':
            $pagina = max(1, (int) ($_GET['pagina'] ?? 1));
            $perPagina = min(100, max(1, (int) ($_GET['per_pagina'] ?? 50)));
            $risultati = elencoPaginato($pdo, $pagina, $perPagina);
            echo json_encode(['success' => true, 'page' => $pagina, 'data' => $risultati]);
            break;

        default:
            http_response_code(404);
            echo json_encode(['error' => 'Endpoint non trovato']);
    }
} catch (Throwable $e) {
    error_log('Errore API: ' . $e->getMessage());
    http_response_code(500);
    echo json_encode(['error' => 'Errore interno del server']);
}

Query parametrizzate e paginazione senza rischi

Ogni valore che arriva dalla query string — il termine di ricerca, il numero di pagina, la dimensione della pagina — va trattato come input non fidato. Le ricerche testuali vanno fatte con parametri bind, anche nelle clausole LIKE, mentre per LIMIT e OFFSET è necessario forzare esplicitamente il tipo intero con bindValue(..., PDO::PARAM_INT): PDO tratta per default tutti i parametri come stringhe, e alcuni driver non accettano stringhe in quelle clausole. È inoltre buona norma imporre un limite massimo alla dimensione di pagina richiesta dal client, per evitare che una singola richiesta metta in difficoltà il database.

Un formato di risposta prevedibile

Indipendentemente dall'endpoint richiesto, conviene restituire sempre la stessa struttura JSON di base, con i campi success, un eventuale count e data, intercettando qualsiasi eccezione imprevista in un blocco try/catch esterno che restituisca un errore 500 generico. Chi consuma l'API può così scrivere un client che gestisce le risposte in modo uniforme, senza dover prevedere formati diversi per ogni endpoint.

Altri articoli su PHP grid_viewTutte le tematiche
Scrivici su WhatsApp