A cura del Prof. Giagnotti · Docente di Informatica e Sistemi e Reti
Guida alla creazione e al test di API REST con PHP
Dal database MySQL alla tua prima API REST completa in PHP: due approcci (procedurale e OOP), sicurezza con prepared statement, CORS e test reali con Thunder Client e Postman.
REST (Representational State Transfer) è un'architettura per sistemi distribuiti: ogni risorsa è un URL, ogni azione è un metodo HTTP, ogni risposta è JSON.
I Web Services REST permettono la comunicazione tra client e server tramite HTTP usando metodi standard:
GET: recupera dati
POST: inserisce dati
PUT: aggiorna dati (sostituzione completa o parziale)
DELETE: elimina dati
Un servizio REST espone API che lavorano su dati in JSON (o XML). È lo standard per collegare app React, Vue, mobile app e qualsiasi frontend al backend.
Principi fondamentali
01 · ARCHITETTURA
Separazione Client/Server
Frontend e backend evolvono indipendentemente. Il frontend consuma solo JSON.
02 · STATELESS
Ogni richiesta è indipendente
Il server non ricorda le richieste precedenti. Tutto il necessario è nell'HTTP request.
03 · ACCESSIBILITÀ
Accessibile via URL
Ogni risorsa ha un endpoint: /utenti, /utenti/5. Semplice da capire e testare.
🎯 Esempio in classe
Quando fate una app in React per la rubrica della scuola, il frontend fa fetch('/rubrica.php'). Non sa se dietro c'è MySQL, file JSON o Firebase. L'API REST è il contratto: “se fai GET ottieni JSON con records”. Questo disaccoppiamento è quello che vi chiedo all'esame di TPSIT.
2. Il progetto rubrica — cosa costruiremo
Realizziamo un servizio WEB con API REST in PHP che interagisce con una tabella utenti attraverso CRUD completo. Ogni utente ha: telefono, cognome, nome, email, via, città, provincia, CAP.
💬 Tip del docente
In classe parto sempre da una rubrica perché è semplice ma completa: ha tutti i tipi di dati che vi servono (stringhe, email, CAP) e vi permette di capire validazione e sanificazione. Se capite la rubrica, potete fare qualsiasi gestionale.
3. Creazione del database MySQL
Apriamo XAMPP, avviamo Apache e MySQL, poi phpMyAdmin cliccando su “Admin” in corrispondenza di MySQL. Creiamo il database rubrica e la tabella utenti.
SQL · Creazione DB e tabella
CREATE DATABASE rubrica;
USE rubrica;
CREATE TABLE utenti(
id INT AUTO_INCREMENT PRIMARY KEY,
cognome VARCHAR(50) NOT NULL,
nome VARCHAR(50) NOT NULL,
email VARCHAR(100),
via VARCHAR(100),
citta VARCHAR(50),
provincia VARCHAR(2),
cap VARCHAR(10),
telefono VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Dimenticare AUTO_INCREMENT sull'id. Senza, la POST fallisce perché MySQL non sa quale id assegnare. E sempre NOT NULL su cognome e nome: sono i due campi che renderemo obbligatori anche in PHP.
4. Approccio procedurale — connessione.php
Creiamo la cartella WebServicePHP_procedurale dentro htdocs di XAMPP. Apriamola in VS Code. Tutti i file andranno qui.
PHP · connessione.php - connessione OOP a MySQLi
<?php
$servername = "localhost";
$username = "root";
$password = "";
$database = "rubrica";
// Connessione in stile OOP
$conn = new mysqli($servername, $username, $password, $database);
// Verifica della connessione
if ($conn->connect_error) {
die(json_encode(["message" => "Connessione fallita: " . $conn->connect_error]));
}
?>
🎯 Perché json_encode nell'errore?
Standardizzare la risposta in JSON è best practice REST. Se usi die("Errore") con testo puro, Thunder Client e Postman mostrano “non-JSON response”. Con json_encode il client sa sempre cosa aspettarsi, anche in caso di errore. All'interrogazione vi chiedo: perché non restituiamo HTML in un'API?
5. Gli header CORS — perché servono davvero
Nella stessa cartella creiamo header.php con gli header che richiameremo in tutti gli script CRUD.
PHP · header.php - Configurazione CORS completa
<?php
// Accesso da qualsiasi dominio
header("Access-Control-Allow-Origin: *");
// Risposta in formato JSON
header("Content-Type: application/json; charset=UTF-8");
// Permette metodi PUT, GET, POST, DELETE
header("Access-Control-Allow-Methods: PUT, GET, POST, DELETE");
// Tempo di caching per le preflight request
header("Access-Control-Max-Age: 3600");
// Header consentiti
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
?>
CORS · ORIGIN
Access-Control-Allow-Origin: *
Rende l'API accessibile da qualsiasi dominio. In produzione limita a domini specifici.
CORS · MAX-AGE
Access-Control-Max-Age: 3600
Il browser memorizza la risposta preflight per 1 ora, evitando OPTIONS ripetute.
CORS · HEADERS
Allow-Headers
Content-Type per JSON, Authorization per token JWT, X-Requested-With per AJAX.
💬 Tip del docente
Spiego sempre la preflight: quando fai PUT o DELETE con header custom, il browser fa prima una richiesta OPTIONS per chiedere “posso?”. Con Max-Age: 3600 dici “sì, per 1 ora non chiedere più”. Riduce traffico e latenza. Se in Thunder Client funziona ma nel browser no, il problema è 90% CORS.
6. Logica CRUD in rubrica.php (procedurale)
Ora creiamo rubrica.php che contiene la logica per i 4 metodi. Prima includiamo connessione, header e leggiamo il metodo e il body.
PHP · rubrica.php - Bootstrap
<?php
// Header comuni
include 'connessione.php';
include_once 'header.php';
// Ottieni il metodo HTTP
$method = $_SERVER['REQUEST_METHOD'];
// Leggi il corpo della richiesta
$input = json_decode(file_get_contents("php://input"), true);
📌 Da ricordare: php://input
file_get_contents("php://input") legge il raw body della richiesta HTTP. Per API JSON è indispensabile: $_POST funziona solo con form-urlencoded, non con JSON.
mysqli_real_escape_string() protegge da SQL Injection escapando caratteri pericolosi come apici. intval() converte in intero qualsiasi input per l'id, bloccando tentativi tipo 1 OR 1=1. In classe mostro sempre un attacco demo: senza sanificazione, un utente malintenzionato cancella tutta la tabella.
PHP · Case PUT - Aggiornamento dinamico con implode
case 'PUT':
if (!empty($input['id'])) {
$id = intval($input['id']);
$updates = [];
foreach (['cognome','nome','email','via','citta','provincia','cap','telefono'] as $field) {
if (isset($input[$field])) {
$value = mysqli_real_escape_string($conn, $input[$field]);
$updates[] = "$field = '$value'";
}
}
if (!empty($updates)) {
$sql = "UPDATE utenti SET " . implode(", ", $updates) . " WHERE id = $id";
if (mysqli_query($conn, $sql)) {
echo json_encode(["message" => "Contatto aggiornato con successo."]);
} else {
echo json_encode(["message" => "Errore durante l'aggiornamento."]);
}
} else {
echo json_encode(["message" => "Nessun campo da aggiornare."]);
}
} else {
echo json_encode(["message" => "ID non fornito."]);
}
break;
implode(", ", $updates) unisce l'array in stringa SQL tipo cognome='Giagnotti', nome='Mario'. È elegante perché aggiorna solo i campi inviati.
PHP · Case DELETE - Eliminazione
case 'DELETE':
if (!empty($input['id'])) {
$id = intval($input['id']);
$sql = "DELETE FROM utenti WHERE id = $id";
if (mysqli_query($conn, $sql)) {
echo json_encode(["message" => "Contatto eliminato con successo."]);
} else {
echo json_encode(["message" => "Errore durante l'eliminazione."]);
}
} else {
echo json_encode(["message" => "ID non fornito."]);
}
break;
default:
http_response_code(405);
echo json_encode(["message" => "Metodo non consentito."]);
break;
}
?>
7. Approccio OOP — classi Database e Rubrica
Ora rifacciamo lo stesso progetto in OOP: più pulito, riutilizzabile e sicuro con prepared statement. Creiamo cartella WebServicePHP_OOP.
connessione.php come classe Database
PHP · Classe Database con getConnection()
<?php
class Database {
private $servername = "localhost";
private $username = "root";
private $password = "";
private $database = "rubrica";
private $conn;
public function getConnection() {
$this->conn = new mysqli($this->servername, $this->username, $this->password, $this->database);
if ($this->conn->connect_error) {
die(json_encode(["message" => "Connessione fallita: " . $this->conn->connect_error]));
}
return $this->conn;
}
}
?>
💬 Tip del docente
Perché una classe Database? Perché domani cambi password o passi a un altro host e modifichi un solo file. In più, puoi riutilizzarla per altri progetti. OOP = meno copia-incolla, più manutenibilità. Ve lo chiedo sempre all'orale: vantaggio dell'incapsulamento.
rubrica.php — classe Rubrica + routing
PHP · Classe Rubrica - proprietà e costruttore
<?php
include_once 'header.php';
include_once 'connessione.php';
class Rubrica {
private $conn;
private $table_name = "utenti";
public $id, $cognome, $nome, $email, $via, $citta, $provincia, $cap, $telefono;
public function __construct($db) {
$this->conn = $db;
}
PHP · Metodi CRUD OOP con prepared statement
public function read() {
$query = "SELECT * FROM " . $this->table_name;
return $this->conn->query($query);
}
public function create() {
$query = "INSERT INTO " . $this->table_name . "
(cognome, nome, email, via, citta, provincia, cap, telefono)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->conn->prepare($query);
$stmt->bind_param("ssssssss", $this->cognome, $this->nome, $this->email, $this->via,
$this->citta, $this->provincia, $this->cap, $this->telefono);
return $stmt->execute();
}
public function update() {
$query = "UPDATE " . $this->table_name . "
SET cognome=?, nome=?, email=?, via=?, citta=?, provincia=?, cap=?, telefono=?
WHERE id=?";
$stmt = $this->conn->prepare($query);
$stmt->bind_param("ssssssssi", $this->cognome, $this->nome, $this->email, $this->via,
$this->citta, $this->provincia, $this->cap, $this->telefono, $this->id);
return $stmt->execute();
}
public function delete() {
$query = "DELETE FROM " . $this->table_name . " WHERE id=?";
$stmt = $this->conn->prepare($query);
$stmt->bind_param("i", $this->id);
return $stmt->execute();
}
}
🎯 Perché prepared statement?
Con prepare + bind_param non serve più real_escape_string: i dati e la query viaggiano separati, SQL Injection impossibile. È lo standard moderno. La “s” in ssssssssi significa string, “i” intero. Se sbagli l'ordine, l'update fallisce silenziosamente — errore tipico in verifica.
Per testare il servizio abbiamo due tool: Thunder Client (estensione VS Code, leggera, senza account) e Postman (più completo, da installare da postman.com/downloads).
Dimenticare di selezionare JSON come tipo di body e non Form. Se selezioni Form, php://input rimane vuoto e ottieni “Dati incompleti”. In Thunder Client scegli Body → JSON. In Postman scegli Body → raw → JSON.
In verifica vi chiedo di testare prima in GET, poi POST, poi PUT con lo stesso id appena creato, poi DELETE. Se fate DELETE di un id inesistente, il mio codice restituisce comunque successo perché MySQL non considera errore una DELETE su 0 righe. Per renderla più robusta, controlla $stmt->affected_rows.
9. Verifica pratica
🔬 Attività — API Rubrica per la scuola
Costruisci l'API completa in entrambe le versioni.
Crea DB rubrica e popola con almeno 6 utenti realistici della tua classe.
Implementa versione procedurale in WebServicePHP_procedurale: connessione, header, rubrica.php con CRUD e sanificazione.
Testa con Thunder Client tutte le operazioni e fai screenshot di GET con records e di POST con messaggio di successo.
Rifattorizza in OOP in WebServicePHP_OOP con prepared statement e confronta la sicurezza.
Aggiungi validazione email con filter_var($email, FILTER_VALIDATE_EMAIL) e restituisci errore JSON se non valida.
✅ Soluzione ragionata
GET deve restituire {"records":[...]}. Se vedi array vuoto, controlla che XAMPP MySQL sia attivo e che il DB sia rubrica. POST deve dare 200 con “Contatto inserito”. Se ottieni “Dati incompleti”, hai dimenticato cognome/nome nel body JSON. PUT senza id → “ID non fornito”. DELETE con id inesistente → comunque “eliminato”, perché la query è tecnicamente riuscita.
Valutazione: 3 punti DB, 4 punti procedurale con header CORS e sanificazione, 4 punti OOP con prepared statement, 3 punti test completi con screenshot, 2 punti bonus per validazione email e controllo affected_rows.