Guida Completa alla Creazione di API REST con PHP: Architettura, Implementazione e Best Practices

Intermedio
API e Servizi Web API REST e GraphQL

Impara a progettare e implementare API REST professionali utilizzando PHP. Dalla gestione degli HTTP header alla sicurezza e l'integrazione con database SQL.

Pubblicato
Tag
PHP Web Development backend REST API JSON PDO

Introduzione alle API REST con 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 industriale. Al centro di questa architettura troviamo le API REST (Representational State Transfer). Un'API REST non è un protocollo, ma uno stile architettonico che utilizza i metodi standard del protocollo HTTP per consentire la comunicazione tra sistemi diversi, indipendentemente dal linguaggio di programmazione utilizzato.

Perché utilizzare PHP per creare API? Sebbene esistano framework moderni come Node.js o Python Fast API, PHP rimane una scelta estremamente solida grazie alla sua ubiquità, alla facilità di deployment e alla potenza di versioni recenti (8.x) che hanno introdotto tipizzazione forte e performance significativamente migliorate. In questo tutorial, esploreremo come costruire un'API REST robusta partendo dalle basi fino a implementare pattern di progettazione professionali.

I Principi Fondamentali di REST

Prima di scrivere codice, è fondamentale capire cosa rende un'API "RESTful". Un'API segue i principi REST se rispetta i seguenti vincoli:

1. Client-Server

Il client (es. un'app React o un'app mobile) e il server (il nostro codice PHP) devono essere indipendenti. Il client non deve conoscere nulla di come i dati sono salvati nel database, e il server non deve conoscere nulla dell'interfaccia utente.

2. Statelessness (Assenza di Stato)

Ogni richiesta inviata dal client al server deve contenere tutte le informazioni necessarie per comprendere e processare la richiesta. Il server non deve memorizzare sessioni (come i classici $_SESSION di PHP) per mantenere lo stato tra una richiesta e l'altra. L'autenticazione avviene solitamente tramite token (come JWT).

3. Interfacciabilità Uniforme

Questo è il punto cruciale. L'interazione avviene tramite:

  • Risorse: Identificate da URL univoci (es. /api/users/1).
  • Metodi HTTP: Definiscono l'azione da compiere.
  • Rappresentazioni: Il formato dei dati scambiati, solitamente JSON.

I Metodi HTTP e i Codici di Stato

Per implementare correttamente un'API, dobbiamo mappare le azioni CRUD (Create, Read, Update, Delete) sui metodi HTTP:

  • GET: Recupera una risorsa o una collezione di risorse.
  • POST: Crea una nuova risorsa.
  • PUT: Aggiorna completamente una risorsa esistente.
  • PATCH: Aggiorna parzialmente una risorsa.
  • DELETE: Rimuove una risorsa.

Allo stesso modo, il server deve rispondere con i codici di stato corretti: 200 OK per il successo, 201 Created dopo un POST, 400 Bad Request per errori di input, 401 Unauthorized per problemi di autenticazione e 404 Not Found quando la risorsa non esiste.

Struttura del Progetto e Configurazione

Per un'API professionale, non possiamo limitarci a un singolo file .php. Dobbiamo organizzare il codice seguendo il principio della singola responsabilità.

Esempio di Struttura Directory

project-api/
├── config/
│   └── database.php
├── src/
│   ├── Controllers/
│   │   └── UserController.php
│   ├── Models/
│   │   └── User.php
│   └── Core/
│       └── Router.php
├── .htaccess
└── index.php

Configurazione del Server (.htaccess)

Per avere URL puliti (es. /users/1 invece di index.php?page=users&id=1), dobbiamo reindirizzare tutte le richieste al file index.php utilizzando il modulo rewrite di Apache.

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Questo file ci permette di implementare un sistema di routing manuale o di utilizzare un framework, centralizzando la gestione delle richieste.

Implementazione Pratica: Il Core dell'API

Iniziamo creando la connessione al database e la logica di base. Useremo PDO (PHP Data Objects) perché è sicuro contro le SQL Injection e supporta diversi database.

Gestione della Connessione (config/database.php)

<?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;
    }
}

Il Modello User (src/Models/User.php)

Il modello si occupa esclusivamente dell'interazione con i dati. Non sa nulla di HTTP o JSON.

<?php
class User {
    private $conn;
    private $table_name = "users";

    public $id;
    public $name;
    public $email;

    public function __construct($db) {
        $this->conn = $db;
    }

    public function readAll() {
        $query = "SELECT id, name, email FROM " . $this->table_name;
        $stmt = $this->conn->prepare($query);
        $stmt->execute();
        return $stmt;
    }

    public function readOne() {
        $query = "SELECT id, name, email FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
        $stmt = $this->conn->prepare($query);
        $stmt->bindParam(1, $this->id);
        $stmt->execute();
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function create() {
        $query = "INSERT INTO " . $this->table_name . " SET name=:name, email=:email";
        $stmt = $this->conn->prepare($query);

        $stmt->bindParam(":name", $this->name);
        $stmt->bindParam(":email", $this->email);

        if($stmt->execute()) {
            return true;
        } return false;
    }
}

Il Controller e l'Entry Point (index.php)

Il controller riceve la richiesta, interroga il modello e restituisce la risposta in formato JSON. È fondamentale impostare l'header Content-Type: application/json affinché il client sappia come interpretare i dati.

<?php
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");

require_once 'config/database.php';
require_once 'src/Models/User.php';

$database = new Database();
$db = $database->getConnection();
$user = new User($db);

$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode('/', $uri);

// Semplice router manuale per l'esempio
if ($uri[2] === 'users') {
    switch ($method) {
        case 'GET':
            if (isset($uri[3]) && is_numeric($uri[3])) {
                $user->id = $uri[3];
                $result = $user->readOne();
                if($result) {
                    echo json_encode($result);
                } else {
                    http_response_code(404);
                    echo json_encode(["message" => "Utente non trovato"]);
                }
            } else {
                $stmt = $user->readAll();
                $users_arr = [];
                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                    extract($row);
                    $user_item = ["id" => $id, "name" => $name, "email" => $email];
                    $users_arr[] = $user_item;
                }
                echo json_encode($users_arr);
            }
            break;

        case 'POST':
            $data = json_decode(file_get_contents("php://input"));
            if(!empty($data->name) && !empty($data->email)) {
                $user->name = $data->name;
                $user->email = $data->email;
                if($user->create()) {
                    http_response_code(201);
                    echo json_encode(["message" => "Utente creato con successo"]);
                } else {
                    http_response_code(503);
                    echo json_encode(["message" => "Errore nel salvataggio"]);
                }
            } else {
                http_response_code(400);
                echo json_encode(["message" => "Dati incompleti"]);
            }
            break;

        default:
            http_response_code(405);
            echo json_encode(["message" => "Metodo non permesso"]);
            break;
    }
} else {
    http_response_code(404);
    echo json_encode(["message" => "Risorsa non trovata"]);
}

Analisi Tecnica: Perché questo approccio?

Gestione dell'Input JSON

Noterete l'uso di file_get_contents("php://input"). A differenza dei form HTML classici, le API REST ricevono i dati nel corpo della richiesta (request body) in formato JSON. $_POST in PHP funziona solo con application/x-www-form-urlencoded o multipart/form-data. Per leggere il JSON grezzo, dobbiamo accedere allo stream di input di PHP.

CORS (Cross-Origin Resource Sharing)

Gli header Access-Control-Allow-Origin: * sono fondamentali. Senza di essi, un browser bloccherà ogni richiesta proveniente da un dominio diverso da quello del server (per motivi di sicurezza). In produzione, è consigliabile sostituire * con il dominio specifico del vostro frontend.

Sicurezza e SQL Injection

L'uso di prepare() e bindParam() di PDO è non negoziabile. Questo processo separa la query SQL dai dati inseriti dall'utente, rendendo impossibile l'iniezione di codice malevolo nel database.

Esempi Pratici e Casi d'Uso

Caso 1: Integrazione con una Single Page Application (SPA)

Immaginiamo di avere un frontend in Vue.js. Il frontend effettuerà una chiamata fetch all'endpoint /users. Il server PHP restituirà un array JSON che Vue.js mapperà in una tabella HTML. Questo permette di aggiornare i dati senza ricaricare l'intera pagina.

Caso 2: Sistema di Autenticazione tramite Token

In un'API reale, non useremmo le sessioni. Implementeremmo un endpoint /login che, dopo aver verificato le credenziali, restituisce un JSON Web Token (JWT). Il client salverà questo token nel localStorage e lo invierà in ogni richiesta successiva nell'header Authorization: Bearer <token>. Il server PHP verificherà la firma del token per ogni richiesta protetta.

Caso 3: Filtri e Paginazione

Per API con migliaia di record, non possiamo restituire tutto in una volta. Implementeremmo query string come /users?page=1&limit=20. In PHP, leggeremmo questi parametri tramite $_GET['page'] e li useremmo nella clausola LIMIT della query SQL.

Errori Comuni e Soluzioni

1. Dimenticare l'Header JSON

Errore: Il client riceve i dati ma li interpreta come testo semplice o HTML, causando errori di parsing nel frontend. Soluzione: Inserire sempre header("Content-Type: application/json") all'inizio del file di ingresso.

2. Confondere PUT e PATCH

Errore: Usare PUT per aggiornare solo un campo (es. solo l'email). Soluzione: Per convenzione, PUT sostituisce l'intera risorsa. Se volete aggiornare solo un dettaglio, usate PATCH e gestite in PHP l'aggiornamento parziale dei campi.

3. Errori PHP che rompono il JSON

Errore: Un Warning o un Notice di PHP viene stampato a video, sporcando l'output JSON e rendendolo non valido. Soluzione: In produzione, disabilitate la visualizzazione degli errori (display_errors = Off nel php.ini) e utilizzate un sistema di logging per registrare gli errori in un file.

Prossimi Passi e Approfondimenti

Ora che avete una base solida per creare API REST con PHP "puro", il passo successivo è scalare la complessità.

  1. Utilizzo di Framework: Se il progetto cresce, implementare un router manuale diventa insostenibile. Studiate Laravel o Symfony. Laravel, in particolare, offre un sistema di routing potentissimo e strumenti come API Resources per trasformare i modelli in JSON in modo elegante.
  2. Documentazione con Swagger/OpenAPI: Un'API senza documentazione è inutile. Imparate a usare Swagger per creare interfacce interattive dove gli sviluppatori frontend possono testare gli endpoint senza scrivere codice.
  3. Implementazione JWT: Approfondite la libreria firebase/php-jwt per aggiungere un livello di sicurezza professionale alle vostre risorse.
  4. Caching: Studiate l'integrazione di Redis per memorizzare le risposte delle API più richieste, riducendo drasticamente il carico sul database SQL.

Costruire API REST efficaci richiede disciplina e attenzione agli standard. Seguendo questi principi, creerete sistemi interoperabili, sicuri e pronti per l'integrazione con qualsiasi tecnologia moderna.