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.