Guida Completa alla Creazione di API REST Professionali in PHP

Intermedio
API e Servizi Web API REST e GraphQL

Impara a progettare e implementare API REST robuste e scalabili utilizzando PHP, seguendo i principi architettonici moderni e le best practice di sicurezza.

Pubblicato
Tag
PHP Web Development backend REST API JSON PDO

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:

  1. php://input: A differenza di $_POST, che funziona solo con form application/x-www-form-urlencoded, php://input permette di leggere il corpo della richiesta a qualsiasi formato, essenziale per ricevere JSON.
  2. CORS (Cross-Origin Resource Sharing): Gli header Access-Control-Allow-Origin sono 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).
  3. Routing Manuale: Abbiamo usato parse_url e explode per 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:

  1. L'utente invia credenziali (email/password) a un endpoint /login.
  2. Il server verifica le credenziali e genera un token JWT firmato con una chiave segreta.
  3. Il server invia il token al client.
  4. Per ogni richiesta successiva, il client invia il token nell'header HTTP: Authorization: Bearer <token>.
  5. 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.