Introduzione alle API REST in PHP
Nel panorama dello sviluppo web moderno, la separazione tra il frontend (l'interfaccia utente) e il backend (la logica di business e i dati) è diventata lo standard. Le API REST (Representational State Transfer) fungono da ponte tra questi due mondi, permettendo a diverse applicazioni — che sia un'app React, un client mobile in Swift o un altro server — di comunicare tra loro in modo standardizzato.
PHP, pur essendo nato come linguaggio di scripting per pagine HTML, si è evoluto enormemente. Oggi è uno strumento potente per costruire servizi backend performanti. Creare un'API REST non significa semplicemente "stampare del JSON", ma implementare un'architettura che rispetti determinati vincoli: statelessness, interfaccia uniforme e identificazione delle risorse tramite URI.
In questa guida esploreremo come costruire un'API da zero, partendo dai concetti base fino alla gestione degli errori e della sicurezza, spiegando non solo come scrivere il codice, ma perché certe scelte architettoniche siano preferibili ad altre.
L'Architettura REST e i Verbi HTTP
Prima di scrivere codice, è fondamentale capire come funziona il protocollo HTTP in un contesto REST. Una risorsa (ad esempio un "Utente" o un "Prodotto") viene identificata da un URL univoco. Le operazioni su questa risorsa vengono definite dai metodi HTTP:
- GET: Recupera una risorsa o una collezione di risorse. Non deve mai modificare lo stato del server.
- POST: Crea una nuova risorsa. I dati vengono inviati nel corpo della richiesta.
- PUT: Aggiorna completamente una risorsa esistente. Se la risorsa non esiste, può crearla.
- PATCH: Aggiorna parzialmente una risorsa. Utile per modificare solo un campo specifico senza inviare l'intero oggetto.
- DELETE: Rimuove una risorsa specifica.
I Codici di Stato HTTP
Un'API professionale non risponde solo con i dati, ma comunica lo stato dell'operazione tramite i codici di stato HTTP. I più comuni sono:
200 OK: Richiesta completata con successo.201 Created: Risorsa creata con successo (tipico del POST).400 Bad Request: La richiesta è malformata o mancano parametri obbligatori.401 Unauthorized: Autenticazione richiesta o fallita.403 Forbidden: L'utente è autenticato ma non ha i permessi per l'azione.404 Not Found: La risorsa richiesta non esiste.500 Internal Server Error: Errore generico del server.
Implementazione Pratica: Struttura di Base
Per creare un'API in PHP senza l'ausilio di framework pesanti come Laravel (per scopi didattici), dobbiamo gestire manualmente gli header di risposta e l'instradamento delle richieste.
Il primo passo è impostare l'header Content-Type: application/json, così che il client sappia che riceverà dati in formato JSON e non HTML.
Ecco un esempio di un router semplificato che gestisce le richieste in base al metodo HTTP e all'endpoint.
<?php
// api.php
// Impostiamo gli header per permettere l'accesso da altri domini (CORS) e definire il tipo di contenuto
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Gestione del metodo OPTIONS per il pre-flight di CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// Recuperiamo il metodo e l'URI della richiesta
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uriSegments = explode('/', trim($uri, '/'));
// Esempio di routing basilare
// URL atteso: /api/users o /api/users/{id}
if ($uriSegments[0] === 'api' && $uriSegments[1] === 'users') {
$userId = $uriSegments[2] ?? null;
switch ($method) {
case 'GET':
if ($userId) {
getUser($userId);
} else {
getUsers();
}
break;
case 'POST':
createUser();
break;
case 'PUT':
if ($userId) {
updateUser($userId);
} else {
http_response_code(400);
echo json_encode(["error" => "ID utente richiesto per l'aggiornamento"]);
}
break;
case 'DELETE':
if ($userId) {
deleteUser($userId);
} else {
http_response_code(400);
echo json_encode(["error" => "ID utente richiesto per la cancellazione"]);
}
break;
default:
http_response_code(405);
echo json_encode(["error" => "Metodo non supportato"]);
break;
}
} else {
http_response_code(404);
echo json_encode(["error" => "Endpoint non trovato"]);
}
// Funzioni di gestione (Mockup per l'esempio)
function getUsers() {
$users = [
["id" => 1, "name" => "Mario Rossi", "email" => "mario@example.com"],
["id" => 2, "name" => "Luigi Bianchi", "email" => "luigi@example.com"]
];
echo json_encode($users);
}
function getUser($id) {
// Simulazione recupero da DB
echo json_encode(["id" => $id, "name" => "Utente Esempio", "email" => "user@example.com"]);
}
function createUser() {
// Recupero dati dal corpo della richiesta (JSON)
$data = json_decode(file_get_contents("php://input"), true);
if (!isset($data['name']) || !isset($data['email'])) {
http_response_code(400);
echo json_encode(["error" => "Dati incompleti"]);
return;
}
http_response_code(201);
echo json_encode(["message" => "Utente creato", "data" => $data]);
}
function updateUser($id) {
$data = json_decode(file_get_contents("php://input"), true);
echo json_encode(["message" => "Utente $id aggiornato", "updated_fields" => $data]);
}
function deleteUser($id) {
http_response_code(200);
echo json_encode(["message" => "Utente $id eliminato con successo"]);
}
Analisi del codice
In questo esempio, abbiamo implementato diversi concetti chiave:
php://input: A differenza di$_POST, che funziona solo con formapplication/x-www-form-urlencoded,php://inputpermette di leggere il corpo della richiesta a qualsiasi formato, essenziale per ricevere JSON.- CORS (Cross-Origin Resource Sharing): Gli header
Access-Control-Allow-Originsono fondamentali. Senza di essi, un browser bloccherebbe le chiamate provenienti da un dominio diverso (es. un frontend su localhost:3000 che chiama un backend su localhost:8000). - Routing Manuale: Abbiamo usato
parse_urleexplodeper simulare un sistema di routing che identifica la risorsa e l'eventuale ID.
Integrazione con il Database (PDO)
Un'API reale non usa array statici. Dobbiamo connetterci a un database. Il modo più sicuro e professionale in PHP è l'utilizzo di PDO (PHP Data Objects), che supporta i prepared statements per prevenire l'SQL Injection.
Ecco come implementare una classe Database e integrarla in una funzione di recupero dati.
<?php
class Database {
private $host = "localhost";
private $db_name = "my_api_db";
private $username = "root";
private $password = "";
public $conn;
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
$this->conn->exec("set names utf8");
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $exception) {
echo "Errore di connessione: " . $exception->getMessage();
}
return $this->conn;
}
}
// Esempio di utilizzo all'interno di getUser()
function getUser($id) {
$database = new Database();
$db = $database->getConnection();
$query = "SELECT id, name, email FROM users WHERE id = :id LIMIT 1";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
if($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode($row);
} else {
http_response_code(404);
echo json_encode(["message" => "Utente non trovato"]);
}
}
Perché usare i Prepared Statements?
L'uso di :id e bindParam assicura che il valore passato dall'utente venga trattato come dato e non come codice SQL. Se un malintenzionato inviasse come ID qualcosa come 1 OR 1=1, l'attacco verrebbe neutralizzato perché PDO scappa automaticamente i caratteri pericolosi.
Sicurezza e Autenticazione
Un'API aperta è un rischio. La maggior parte delle API REST utilizza i JWT (JSON Web Tokens) per l'autenticazione stateless. A differenza delle sessioni tradizionali (che richiedono l'uso di cookie e storage lato server), il JWT contiene tutte le informazioni necessarie all'interno del token stesso, firmato digitalmente dal server.
Come funziona il flusso JWT:
- L'utente invia credenziali (email/password) a un endpoint
/login. - Il server verifica le credenziali e genera un token JWT firmato con una chiave segreta.
- Il server invia il token al client.
- Per ogni richiesta successiva, il client invia il token nell'header HTTP:
Authorization: Bearer <token>. - Il server valida la firma del token e, se corretta, autorizza l'operazione.
Validazione dei Dati
Non fidarti mai dell'input dell'utente. Oltre a PDO, è necessario validare i dati in ingresso utilizzando funzioni come filter_var():
$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
if (!$email) {
http_response_code(400);
echo json_encode(["error" => "Email non valida"]);
exit();
}
Esempi Pratici: Casi d'Uso Reali
Caso 1: Catalogo Prodotti per E-commerce
Immaginiamo di dover fornire i prodotti a un frontend in Vue.js. L'API dovrà gestire filtri e paginazione. L'endpoint sarà GET /api/products?category=electronics&page=1.
Nel backend, leggeremo i parametri tramite $_GET['category'] e $_GET['page'], costruendo una query SQL dinamica con un LIMIT e OFFSET per gestire la paginazione, restituendo non solo la lista prodotti, ma anche il numero totale di pagine.
Caso 2: Sistema di Commenti in Tempo Reale
Per un blog, l'API dovrà permettere l'aggiunta di commenti (POST /api/comments) e l'aggiornamento del testo (PATCH /api/comments/{id}). In questo caso, l'uso di PATCH è fondamentale: l'utente vuole cambiare solo il testo del commento, non deve rispedire l'ID dell'autore o la data di creazione.
Errori Comuni e FAQ
1. Perché ricevo l'errore "CORS" nel browser?
Il browser blocca la risposta se il server non invia l'header Access-Control-Allow-Origin. Assicurati di includerlo all'inizio del tuo script PHP.
2. Perché $_POST è vuoto quando invio JSON?
$_POST viene popolato solo se il Content-Type è application/x-www-form-urlencoded o multipart/form-data. Per il JSON, devi usare file_get_contents("php://input") e poi json_decode().
3. Differenza tra PUT e PATCH?
PUT sostituisce l'intera risorsa. Se invii un oggetto PUT con solo un campo, gli altri campi nel database potrebbero essere sovrascritti con valori nulli o vuoti. PATCH aggiorna solo i campi specificati.
Prossimi Passi e Approfondimenti
Costruire un'API in PHP "puro" è un ottimo esercizio per capire i fondamenti, ma per progetti di produzione su larga scala, è consigliabile utilizzare framework che implementano già questi pattern in modo ottimizzato e sicuro.
Risorse consigliate:
- Laravel: Il framework PHP più popolare, con un sistema di routing, middleware e Eloquent ORM che rendono la creazione di API REST estremamente veloce.
- Slim Framework: Un micro-framework ideale se hai bisogno di qualcosa di leggero, specifico per la creazione di API, senza l'overhead di un framework completo.
- Swagger/OpenAPI: Impara a documentare le tue API. Swagger permette di creare una pagina interattiva dove gli altri sviluppatori possono testare i tuoi endpoint senza scrivere codice.
- Postman: Utilizza questo strumento per testare i tuoi endpoint, gestire le collezioni di richieste e simulare l'invio di token JWT.
In conclusione, la creazione di API REST in PHP richiede attenzione alla semantica HTTP, alla sicurezza dei dati e alla gestione corretta degli errori. Seguendo questi principi, potrai costruire backend scalabili capaci di supportare qualsiasi tipo di client moderno.