Créer une API REST en PHP
Un guide pas à pas pour apprendre les bases d’une API REST en PHP, sans framework, avec un environnement moderne (FrankenPHP, MariaDB, Docker ou installation classique).
Ce article est destiné aux débutants, et offre aussi une première découverte de la notion d'API REST aux intermédiaires.
1. Préparer l’environnement
Avant de commencer, il te faut:
- FrankenPHP (serveur web moderne avec PHP intégré)
- MariaDB ou MySQL (en local, ou via Docker)
- (Optionnel) Adminer pour administrer ta base plus facilement
Astuce: si tu n’as pas MariaDB/MySQL installé, Docker est la solution la plus simple pour tester sans rien casser sur ton système.
1.1. Installer FrankenPHP
L’installation est ultra simple. Ouvre ton terminal et lance:
curl https://frankenphp.dev/install.sh | sh
sudo mv frankenphp /usr/local/bin/
Résultat : FrankenPHP est installé et disponible dans ton terminal avec la commande frankenphp
.
1.2. Teste l'installation
frankenphp --version
Si tu vois s’afficher la version, c’est tout bon!
1.3. Créer le dossier projet
On va organiser notre travail proprement dès le départ.
- Crée un dossier pour ton projet (par exemple
learn-php
) :
mkdir learn-php
cd learn-php
- À la racine, ajoute un fichier vide
compose.yaml
(on y viendra juste après) :
touch compose.yaml
- Crée un dossier
public/
pour y mettre ton premier fichier PHP :
mkdir public
echo "<?php echo 'bonjour le monde';" > public/index.php
Arborescence obtenue
learn-php/
├── compose.yaml
└── public/
└── index.php
1.4. Préparer la base de données avec Docker
Pour simplifier, on va utiliser Docker afin d’éviter d’installer une base de données “en dur” sur ta machine.
Pas de panique: il suffit d’installer Docker Desktop (Mac/Windows/Linux, c’est gratuit pour un dev (liste des restrictions ici) :) ).
Mais si tu as déjà MariaDB ou MySQL installé localement, ça fonctionne aussi – adapte juste les paramètres de connexion.
Et si tu as peur de docker, tu peux allez lire mon guide Docker pour les nuls pour comprendre les bases.
Dans ton dossier projet (learn-php
), crée un fichier si ce n’est pas déjà fait, compose.yaml
contenant:
services:
mariadb:
image: mariadb:10.11
container_name: api-php-db
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: api_php
MYSQL_USER: userapi
MYSQL_PASSWORD: passapi
ports:
- "3308:3306"
volumes:
- mariadb_api_data:/var/lib/mysql
adminer:
image: adminer
container_name: api-php-adminer
restart: always
ports:
- "8083:8080"
volumes:
mariadb_api_data:
Explication :
- On démarre un conteneur MariaDB avec un mot de passe root, une base de données
api_php
et un utilisateuruserapi
avec son mot de passepassapi
. - On expose le port 3308 pour accéder à MariaDB (au lieu du port 3306 par défaut, pour éviter les conflits si tu as déjà une base locale).
- On démarre un conteneur Adminer pour administrer la base de données via un navigateur (facultatif, mais pratique).
- On crée un volume persistant pour conserver les données de la base même après arrêt du conteneur.
Lancer la base de données :
docker-compose up --build
va sur http://localhost:8083/ tu devrais te connecter à la base de données avec les paramètres ci-dessus.
Ton setup de départ


Et voilà
KISS
, même pas besoin de galérer avec mille outils: un terminal, un PHP moderne (merci FrankenPHP), et ta base tourne dans un conteneur Docker. Prêt à coder dans de bonnes conditions!
2. Petit rappel : comment fonctionne PHP avec HTML?
Avant de plonger dans les API REST, revoyons comment PHP et HTML interagissent traditionnellement sur le web.
Exemple simple : un formulaire qui transmet une donnée à PHP
index.php
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Interaction HTML & PHP</title>
</head>
<body>
<form action="./process.php" method="post">
<label for="username">Votre nom :</label>
<input type="text" name="username" id="username" placeholder="Entrez votre nom">
<button type="submit">Envoyer</button>
</form>
</body>
</html>
process.php
<?php
declare(strict_types=1);
function validate_username(?string $username): ?string
{
$username = trim((string)$username);
if (
$username === '' ||
mb_strlen($username) < 2 ||
mb_strlen($username) > 32 ||
!preg_match('/^[\p{L}\p{N} \-\'éèêëàâäôöùûüç]+$/u', $username)
) {
return null;
}
return $username;
}
$username = $_POST['username'] ?? null;
if ($username === null) {
echo "Aucun nom reçu.";
return;
}
$validated = validate_username($username);
if ($validated === null) {
echo "Nom invalide.";
return;
}
$safeUsername = htmlspecialchars($validated, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
echo "Bonjour, {$safeUsername} !";
Résultat : si tu tapes et envoies un nom depuis le formulaire, PHP va le valider et l’afficher.
3. Contexte :
C’est quoi une API REST et à quoi ça sert?
Une API (Application Programming Interface), c’est une “porte d’entrée” qui permet à un logiciel d’en piloter un autre, d’échanger des données, ou de lui demander de faire une action… le tout automatiquement, sans interface graphique.
Sur le web, les APIs sont partout: applis mobiles, sites, objets connectés… REST (Representational State Transfer) est le style d’API le plus répandu pour dialoguer entre serveurs et clients via Internet.
Les différents types d’API:
- API REST : Utilise le protocole HTTP pour échanger des données (JSON, XML…)
- API SOAP : Utilise un langage de description de services (WSDL) et XML pour les échanges
- API GraphQL : Permet de demander exactement ce dont on a besoin, avec une syntaxe puissante
Mais ici, on va se concentrer sur les APIs REST, car c’est le plus courant sur le web.
Concrètement, une API REST:
- Permet à deux programmes de communiquer par des requêtes HTTP (GET, POST, PUT, DELETE…)
- Échange des données (souvent au format JSON)
- Suit des règles et conventions pour faciliter l’intégration et l’automatisation
4. Concepts théoriques à connaître
Voici quelques notions clés pour bien comprendre et utiliser une API REST:
-
Ressource: c’est une “entité” manipulée via l’API (ex: un utilisateur, un article, un produit…).
-
Endpoint: une “adresse” de l’API pour accéder à une ressource (ex:
/api/users/42
) -
Méthodes HTTP:
-
GET
: récupérer une ressource -
POST
: créer -
PUT
/PATCH
: modifier ou plus précisémentPUT
pour remplacer complètement,PATCH
pour modifier partiellement -
DELETE
: supprimer -
Statuts HTTP: codes de réponse comme
200 OK
,201 Created
,400 Bad Request
,404 Not Found
... -
JSON: format de données utilisé pour communiquer entre client et API
Exemple:
{"nom": "John", "age": 30}
-
Stateless: en français, ça veut dire sans état. Chaque requête est indépendante de celles qui la précèdent ou la suivront. En d’autres termes, chaque requête doit contenir toutes les infos nécessaires, car l’API ne “se souvient” de rien entre deux appels
Exemple: si tu veux supprimer un utilisateur, tu dois envoyer son identifiant dans la requête, car l’API ne sait pas qui est l’utilisateur actuel.
-
RESTful: on parle d’API RESTful quand on respecte ces conventions de manière cohérente
-
Versionning d’API: L’API peut évoluer dans le temps. Pour garantir la compatibilité, on ajoute souvent une version dans le chemin, comme
/api/v1/users
. Cela permet de proposer plusieurs versions d’une API sans casser les anciens clients.
Exemple de cas d'utilisation d'une API:
- une appli mobile qui affiche une liste de produits, et qui permet de les ajouter au panier. L'appli mobile communique avec l'API pour récupérer la liste des produits, et pour ajouter un produit au panier.
- Un site Angular ou React qui affiche le solde d'un compte bancaire. Le site communique avec l'API pour récupérer le solde du compte.
5. Exercice fil rouge
Assez de théorie, place à la pratique: on va développer une mini API REST de A à Z!
Scénario:
Imagine que tu dois créer une appli pour la DSI d’Althéria, le fameux royaume numérique.
L’objectif? Mettre à disposition une API simple pour gérer la liste des souverains:
- Ajouter un souverain (POST)
- Lister tous les souverains (GET)
- Supprimer un souverain (DELETE)
- [Bonus] Mettre à jour un nom ou une date de règne (PUT/PATCH)
Chaque souverain possède :
- un identifiant unique (id)
- un nom
- une province
- une date de début de règne
On va donc construire:
- une base de données
souverains
- un endpoint d’API
/api/souverains
- plusieurs routes HTTP pour manipuler les souverains
But de l’exercice : À la fin, tu pourras gérer tout le cycle de vie d’un souverain dans la base, via ton API.
Prêt à faire ton premier “backend” pour une application moderne? On commence par créer la table et préparer la connexion à la base de données!
6. Préparer la base de données
Avant tout, connectons-nous à la base pour créer notre première table!
Accéder à Adminer
- Lance Adminer (si tu utilises Docker Compose comme conseillé plus haut, le conteneur doit déjà tourner)
- Ouvre ton navigateur à l’adresse: http://localhost:8083
- Renseigne les infos de connexion :
- SGBD:
MariaDB/ MySQL
- Serveur:
api-php-db
- Utilisateur:
userapi
- Mot de passe:
passapi
- Base:
api_php
- Clique sur “Connexion”.
Créer la table “souverains”
Avant de manipuler des données, il faut structurer la base. Ici, on va utiliser le SCD Type 2 pour garder l’historique des changements de souverains (par exemple, qui a été roi, quand et pour combien de temps).
Curieux d’en savoir plus sur le SCD Type 2 et la gestion de l’historique dans une BDD? Consulte mon article dédié au SCD Type 2 & 4 dans MySQL pour découvrir le concept à travers un exemple fun façon Game of Thrones !
Une fois connecté dans Adminer:
- Va dans “SQL”
- Copie/colle la requête suivante pour créer la table :
CREATE TABLE souverains (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
province VARCHAR(100) NOT NULL,
date_debut DATE NOT NULL,
date_fin DATE DEFAULT NULL,
is_current BOOLEAN DEFAULT TRUE
);
Tu dois voir quelque chose comme ça:

Mise en place du projet : Structure, outils et premiers scripts
- Préparer la structure du projet (MVC)
Organisons notre projet en suivant le modèle MVC simplifié:
learn-php/
├── public/ # Point d’entrée (index.php)
├── src/
│ ├── Controller/ # Logique métier
│ ├── Model/ # Entités PHP (Souverain.php)
│ └── View/ # Vues (les templates HTML)
├── .env # Variables d’environnement (BDD…)
├── composer.json # Dépendances PHP
└── compose.yml # (si tu as utilisé Docker comme conseillé, pour la BDD)
(tu es perdu, relax) posons quelques concepts théoriques.
Allez, c'est simple ! Le point d'entrée de notre application est le fichier index.php, qui joue le rôle de routeur principal.
Lorsqu'un utilisateur sollicite une route spécifique, le routeur se charge d'interroger le contrôleur concerné. Ce dernier, à son tour, fait appel au modèle pour traiter les données en s'appuyant sur l'entité correspondante. (T'inquiète, ça va venir !)
Tu as compris l'interêt d'avoir les concepts theorique, ils sont ultra importants donc un petit résumé :
Le MVC est un design pattern qui permet de séparer la logique de l'application en trois parties distinctes : le modèle, la vue et le contrôleur.
- Les modèles sont responsables de la gestion des données, des entités, des requêtes SQL, etc.
- Les vues sont responsables de l'affichage des données, de la génération des pages HTML, etc.
- Les contrôleurs sont responsables de la gestion des requêtes, de la logique métier, de l'interaction avec les modèles et les vues, etc.
Et oui, j'ai compris et tu as raison ! dans le cas d'une API REST, on n'a pas besoin de vues HTML car on renvoie des données JSON à notre frontend (Angular, React, Vue, etc.), mais on a toujours besoin de modèles et de contrôleurs.
Mais pour l'instant, on va garder notre dossier
View/
pour la documentation de l'API, mais on y reviendra plus tard.
En français facile :
Le contrôleur, c'est un peu comme la boîte de vitesses d'une voiture qui attend les instructions du chauffeur puis transmet un comportement au moteur.
Les modèles, c'est la boîte à essence qui fournit l'énergie au moteur.
Les vues, c'est le tableau de bord qui affiche les informations au chauffeur.
Maintenant, parlons de Composer. Tu vois que j'ai ajouté un fichier composer.json.
Pas de panique : Composer est un gestionnaire de dépendances pour PHP. C'est un outil qui permet de gérer les dépendances de ton projet, c'est-à-dire les librairies que tu utilises.
Tu vas peut-être me demander : "Mais c'est quoi une librairie ?"
Une librairie, c'est un ensemble de fonctions qui permettent de réaliser certaines tâches. Par exemple, si tu veux envoyer un mail, tu peux utiliser la librairie PHPMailer qui te permet de le faire très simplement, sans avoir à tout reprogrammer toi-même.
Un exemple typique dans notre cas :
- On a besoin de lire des variables d'environnement, alors on utilise la librairie vlucas/phpdotenv qui nous facilite la tâche.
- On doit remplir notre table
souverain
avec des données fictives; au lieu de le faire à la main, on utilise fakerphp/faker pour générer ces données automatiquement. - Pour construire un bon routeur (le fameux
index.php
), on utilise la librairie nikic/fast-route qui simplifie la gestion des routes. - Plus tard, pour documenter notre API, on utilisera swagger-php qui permet de générer la documentation facilement.
Résultat : dans le fichier composer.json
, on met tout simplement ça :
{
"name": "pabiosoft/learn-php-api",
"description": "Mini API REST avec FrankenPHP, FastRoute et Dotenv",
"require": {
"nikic/fast-route": "^1.3",
"vlucas/phpdotenv": "^5.5",
"fakerphp/faker": "^1.24",
"zircote/swagger-php": "^4.0"
},
"autoload": {
"psr-4": {
"Pabiosoft\\": "src/"
}
}
}
Et voilà, on est prêt à coder ! Ou presque… encore un dernier concept : tu vois la clé autoload
dans le fichier composer.json
?
Tu te demandes ce que ça peut bien faire?
Souviens-toi, lors de tes premières utilisations de PHP, tu avais besoin de faire des include
ou des require
pour importer des fichiers?
Nous, on veut éviter ça: on souhaite que PHP charge automatiquement les fichiers dont on a besoin. C’est ça, l’autoload.
On va utiliser le système de chargement automatique PSR-4, très populaire dans la communauté PHP.
On indique à Composer que tout ce qui est dans le dossier src/
doit être chargé automatiquement en utilisant l’espace de nom Pabiosoft\
.
Donc, au lieu de faire:
include 'src/Controller/SouverainController.php';
on pourra simplement écrire:
use Pabiosoft\Controller\SouverainController;
C’est pratique, non?
Cette fois ci on va installer les dépendances avec la commande suivante:
composer install
assure toi d'être dans le dossier learn-php a la racine du projet.
Et voilà: si tu vois le dossier vendor
apparaître dans ton projet, c’est que tout est bon!
Sinon, si tu n’as jamais installé Composer sur ton ordinateur, pas de panique: tu peux l’installer très facilement en suivant les instructions sur le site officiel: https://getcomposer.org/download/
Ou directement sur macOS:
brew install composer
sous windows et linux:
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
Cette fois-ci, tu es prêt: relance la commande composer install
et tu devrais voir le dossier vendor
apparaître.
Allez, on peut commencer à coder: assez de théorie pour aujourd’hui!
Actuellement, on a une base de données avec une table vide. Commençons par créer un contrôleur pour y insérer des données. Le modèle:
Dans le dossier src/Model
, crée un fichier Database.php
et ajoute le code suivant:
<?php
// src/Model/Database.php
namespace Pabiosoft\Model;
use Dotenv\Dotenv;
use PDO;
use PDOException;
class Database
{
private static ?PDO $pdo = null;
public static function getConnection(): PDO
{
if (self::$pdo === null) {
// Charger les variables d'environnement (.env)
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->safeLoad();
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
$port = $_ENV['DB_PORT'] ?? '3308';
$db = $_ENV['DB_NAME'] ?? 'api_php';
$user = $_ENV['DB_USER'] ?? 'userapi';
$pass = $_ENV['DB_PASSWORD'] ?? 'passapi';
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
try {
self::$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
die('Erreur connexion BDD: ' . $e->getMessage());
}
}
return self::$pdo;
}
}
Tu remarques qu’on utilise Dotenv\Dotenv
pour récupérer les variables d’environnement.
Il faut donc créer un fichier .env
à la racine du projet et y définir les variables nécessaires.
DB_HOST=127.0.0.1
DB_PORT=3308
DB_NAME=api_php
DB_USER=userapi
DB_PASSWORD=passapi
Puis toujours dans le dossier src/Model
, crée un fichier Souverain.php
et ajoute le code suivant:
<?php
// src/Model/Souverain.php
namespace Pabiosoft\Model;
class Souverain
{
public ?int $id;
public string $nom;
public string $province;
public string $date_debut;
public ?string $date_fin;
public bool $is_current;
public function __construct(array $data)
{
$this->id = $data['id'] ?? null;
$this->nom = $data['nom'];
$this->province = $data['province'];
$this->date_debut = $data['date_debut'];
$this->date_fin = $data['date_fin'] ?? null;
$this->is_current = (bool) ($data['is_current'] ?? true);
}
}
Pas d’inquiétude pour la gestion des dates, un chapitre complet y sera consacré plus tard dans ce guide.
Le controller:
Dans le dossier src/Controller
, crée un fichier SouverainController.php
et ajoute le code suivant:
<?php
// src/Controller/SeedController.php
namespace Pabiosoft\Controller;
use Pabiosoft\Model\Database;
use Faker\Factory;
class SeedController
{
public static function seedSouverains(int $regnesParProvince = 4): array
{
$pdo = Database::getConnection();
$faker = Factory::create('fr_FR');
$provinces = ['Solaria', 'Umbra', 'Néréïde', 'Argath', 'Célestia'];
$created = [];
foreach ($provinces as $province) {
$date_debut = $faker->dateTimeBetween('-30 years', '-10 years');
for ($i = 0; $i < $regnesParProvince; $i++) {
$nom = $faker->firstName . ' ' . $faker->lastName;
// Le prochain règne démarre à la date de fin du précédent
$duree = $faker->numberBetween(2, 8) . ' years';
$next_date_debut = (clone $date_debut)->modify("+$duree");
$date_fin = $i < $regnesParProvince - 1 ? $next_date_debut->format('Y-m-d') : null; // Dernier règne pas de fin
$is_current = $i === $regnesParProvince - 1 ? 1 : 0;
$stmt = $pdo->prepare(
"INSERT INTO souverains (nom, province, date_debut, date_fin, is_current) VALUES (?, ?, ?, ?, ?)"
);
$stmt->execute([
$nom,
$province,
$date_debut->format('Y-m-d'),
$date_fin,
$is_current
]);
$created[] = [
'nom' => $nom,
'province' => $province,
'date_debut' => $date_debut->format('Y-m-d'),
'date_fin' => $date_fin,
'is_current' => $is_current,
];
$date_debut = $next_date_debut;
}
}
return $created;
}
}
Et oui, on a utilisé le M et le C du modèle MVC. Mais pour que le contrôleur soit exploité, il faut l’appeler dans notre index.php
, qui est la première page chargée par le serveur (FrankenPhp).
Dans le dossier public/
, crée un fichier index.php
et ajoute le code suivant:
<?php
declare(strict_types=1); // On active le mode strict PHP
require_once __DIR__ . '/../vendor/autoload.php';
use Pabiosoft\Controller\SeedController;
use FastRoute\RouteCollector;
$dispatcher = FastRoute\simpleDispatcher(function (RouteCollector $r) {
// Page d'accueil minimaliste
$r->addRoute('GET', '/', function () {
echo '<h2>Bienvenue sur l’API REST PHP minimaliste!</h2>';
});
// Route pour peupler la BDD avec Faker
$r->addRoute('GET', '/seed', function () {
// On seed 4 règnes par province (modifiable dans SeedController)
$created = SeedController::seedSouverains(4);
header('Content-Type: Pabiosoftlication/json');
echo json_encode([
'count' => count($created),
'souverains' => $created
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
});
});
/***
* Cette partie Dispatch ne changera plus.
* Elle sert à dispatcher la requête HTTP vers la bonne route.
* Ce sont les routes que l'on définit ci-dessus.
*/
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo "404 - Not Found";
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
http_response_code(405);
echo "405 - Method Not Allowed";
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
$handler($vars);
break;
}
Ça y est, on vient de créer deux endpoints (routes) API via ce fichier index.php
: /
et /seed
, qui va charger nos données dans la base de données.
C’est exactement ce que fait cette fonction callback.
$dispatcher = FastRoute\simpleDispatcher(function (RouteCollector $r) {
// Page d'accueil minimaliste
$r->addRoute('GET', '/', function () {
echo '<h2>Bienvenue sur l’API REST PHP minimaliste!</h2>';
});
// Route pour peupler la BDD avec Faker
$r->addRoute('GET', '/seed', function () {
// On seed 4 règnes par province (modifiable dans SeedController)
$created = SeedController::seedSouverains(4);
header('Content-Type: Pabiosoftlication/json');
echo json_encode([
'count' => count($created),
'souverains' => $created
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
});
});
On vient d’utiliser le M et le C du modèle MVC. Il reste le V, mais on n’en aura pas besoin pour l’instant.
Tu te demandes comment tester ton API?
Rappelle-toi: notre API n’a pas besoin de page HTML pour fonctionner, elle renvoie des données en JSON. On peut aussi insérer des données JSON directement, sans passer par une page HTML.
Pour tout ça, on va utiliser des outils comme Postman ou Insomnia, mais je te recommande Apidog : ce sont des applications gratuites et puissantes pour tester tes APIs, sans avoir besoin d’un frontend (Javascript,React, Angular, Vue, etc.).
C’est ce qu’on va faire ici.
Apres installation il suffit de creer un projet et d’ajouter tes routes et puis le bouton send.

[GET] http://localhost
via Apidog: tout fonctionne, l’API répond bien!et la route pour remplir notre base de données:

[GET] http://localhost/seed
via Apidog: la base de données est bien remplie !Et voilà, on a une API REST en PHP qui fonctionne et qui est capable de remplir notre base de données avec des données fictives. Si tu fais un tour dans Adminer, tu devrais voir les données qui ont été insérées.

[GET] http://localhost/seed
via Apidog: les données sont bien insérées !Parfait, on avance! Passons à la création des routes pour consulter les souverains.
- La route
[GET] /api/souverains
permetra d’obtenir la liste complète des souverains. - La route
[GET] /api/souverains/{id}
permetra d’afficher un souverain précis.
À chaque fois, le principe est simple: on crée un contrôleur (handler) avec une unique fonction __invoke()
qui contient la logique de la route.
Ensuite, on branche ce contrôleur dans le fichier index.php
, et le tour est joué.
Commençons par la route [GET] /api/souverains
, qui permet d’obtenir la liste complète des souverains.
Dans le dossier src/Controller
, crée un fichier GetSouverainsController.php
et ajoute le code suivant:
<?php
// src/Controller/GetSouverainsController.php
namespace Pabiosoft\Controller;
use Pabiosoft\Model\Database;
class GetSouverainsController
{
public function __invoke(): void
{
$pdo = Database::getConnection();
$stmt = $pdo->query("SELECT id, nom, province, date_debut, date_fin, is_current FROM souverains ORDER BY province, date_debut");
$souverains = $stmt->fetchAll();
header('Content-Type: application/json; charset=utf-8');
echo json_encode($souverains, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
}
Ensuite, crée la route pour consulter un souverain précis avec [GET] /api/souverains/{id}
.
Dans le dossier src/Controller
, crée un fichier GetOneSouverainController.php
et ajoute le code suivant:
<?php
// src/Controller/GetSouverainController.php
namespace Pabiosoft\Controller;
use Pabiosoft\Model\Database;
class GetOneSouverainController
{
public function __invoke(array $vars): void
{
$id = (int) ($vars['id'] ?? 0);
$pdo = Database::getConnection();
$stmt = $pdo->prepare("SELECT id, nom, province, date_debut, date_fin, is_current FROM souverains WHERE id = ?");
$stmt->execute([$id]);
$souverain = $stmt->fetch();
header('Content-Type: application/json; charset=utf-8');
if ($souverain) {
echo json_encode($souverain, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} else {
http_response_code(404);
echo json_encode(['error' => "Aucun souverain trouvé avec l’id $id."]);
}
}
}
Et voilà, il ne reste plus qu’à brancher ces routes dans le fichier index.php
.
Juste en dessous de la route /seed
, ajoute les routes suivantes:
$r->addRoute('GET', '/souverains', function() {
(new \Pabiosoft\Controller\GetSouverainsController())->__invoke();
});
$r->addRoute('GET', '/souverains/{id:\d+}', function($vars) {
(new \Pabiosoft\Controller\GetOneSouverainController())->__invoke($vars);
});
Petite explication: la librairie FastRoute gère facilement les routes dynamiques, c’est-à-dire celles qui incluent des variables dans leur chemin.
Sa méthode addRoute
accepte une expression régulière pour filtrer la variable: ici, on utilise \d+
pour indiquer que l’id doit être composé d’un ou plusieurs chiffres.
Résultat: nos deux routes pour consulter les souverains sont en place!
Tu peux maintenant les tester directement via Apidog.

[GET] http://localhost/souverains/{id}
via Apidog: la réponse affiche bien le souverain demandé!Pour tester la route /souverains
qui renvoie la liste complète, retire simplement le {id}
(ici le chiffre 1) de l’URL et envoie la requête: tu obtiens tous les souverains enregistrés dans la base, sous forme d’un tableau d’objets JSON.
Nous avons réussi à récupérer les données, mais comment les insérer?
Pour cela, il faut créer une route dédiée qui permet d’ajouter un nouveau souverain, par exemple lorsqu’un ancien est destitué ou décédé. Attention: il ne doit jamais y avoir deux souverains actifs en même temps dans une même province (règle SCD Type 2).
La logique reste la même: on crée un contrôleur pour gérer l’insertion, puis on branche la route dans le fichier index.php
.
Commençons par le contrôleur: crée le fichier CreateSouverainController.php
dans le dossier src/Controller
et ajoute le code suivant:
<?php
namespace Pabiosoft\Controller;
use Pabiosoft\Model\Souverain;
use Pabiosoft\Model\Database;
class CreateSouverainController
{
public function __invoke()
{
$data = json_decode(file_get_contents('php://input'), true);
// Validation basique
if (!isset($data['nom'], $data['province'], $data['date_debut'])) {
http_response_code(400);
echo json_encode(['error' => 'Données incomplètes.']);
return;
}
$province = $data['province'];
$is_current = $data['is_current'] ?? true;
$date_debut = $data['date_debut'];
$date_fin = $data['date_fin'] ?? null;
$pdo = Database::getConnection();
// Si le nouveau souverain est actif/is_current, désactiver l'ancien actif dans la province
if ($is_current) {
// 1. Mettre à jour tous les souverains actifs dans cette province pour les passer à inactif (is_current=0)
$stmt = $pdo->prepare("UPDATE souverains SET is_current = 0, date_fin = ? WHERE province = ? AND is_current = 1");
$stmt->execute([$date_debut, $province]);
}
// 2. On vérifie qu'aucun souverain n'a une période active qui se chevauche (optionnel)
// (Tu peux améliorer ici si tu veux des règles plus strictes)
// 3. On insère le nouveau souverain
$souverain = new Souverain([
'id' => null,
'nom' => $data['nom'],
'province' => $data['province'],
'date_debut' => $data['date_debut'],
'date_fin' => $data['date_fin'] ?? null,
'is_current' => $data['is_current'] ?? true,
]);
$id = $souverain->save();
http_response_code(201);
echo json_encode(['id' => $id, 'message' => 'Souverain créé avec succès, et les précédents ont été désactivés.']);
}
}
Comme tu peux le voir, on utilise une méthode save()
qui n’existe pas encore: il faut donc l’ajouter dans le fichier Souverain.php
du dossier src/Model
.
//...
public function save(): int
{
$pdo = Database::getConnection();
$stmt = $pdo->prepare("INSERT INTO souverains (nom, province, date_debut, date_fin, is_current) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$this->nom,
$this->province,
$this->date_debut,
$this->date_fin,
$this->is_current,
]);
return (int) $pdo->lastInsertId();
}
Et voilà, tu as compris la logique: il ne reste plus qu’à ajouter la route dans le fichier index.php
, juste après les deux routes GET pour les souverains.
$r->addRoute('POST', '/souverains', function() {
(new \Pabiosoft\Controller\CreateSouverainController())->__invoke();
});
Et c’est le moment de tester dans Apidog! On va simplement envoyer un JSON contenant les données du nouveau souverain.
{
"nom": "Daenerys Targaryen",
"province": "Solaria",
"date_debut": "2022-07-24"
}
En résultat, si tu obtiens ceci, c’est que tout fonctionne:
{
"id": 21,
"message": "Souverain créé avec succès, et les précédents ont été désactivés."
}

[POST] http://localhost/souverains
via Apidog: le nouveau souverain est bien créé et la réponse s’affiche!Ça y est, tu es maintenant prêt! Amuse-toi à créer la route pour modifier un souverain et celle pour le supprimer.
Astuce: pour supprimer, envoie une requête DELETE depuis Apidog; pour modifier, utilise une requête PUT (afin de respecter le principe RESTful).
Je te fais confiance: tu as acquis la logique et, surtout, les concepts théoriques essentiels de nos jours.
Bonus: la documentation de l’API
Pour terminer cette conception, on va enfin exploiter le “V” du MVC: la vue.
Tu te rappelles qu’on a ajouté la librairie zircote/swagger-php
dans le composer.json
? On va l’utiliser pour documenter notre API, car une API REST sans documentation est inutilisable.
Le principe est simple: une fois ton API terminée, tu ajoutes des annotations dans ton code pour décrire tes routes, paramètres, requêtes, réponses, statuts, etc.
Tout cela sera généré automatiquement dans un fichier openapi.yaml
, utile aussi bien pour toi dans le futur que pour les développeurs frontend qui voudront consommer ton API.
C’est parti: dans le dossier src/View
, crée un fichier OpenApiInfo.php
et ajoute le code suivant:
<?php
namespace Pabiosoft\View;
use OpenApi\Attributes as OA;
#[OA\Info(
version: '1.0.1',
title: 'API des souverains'
)]
#[OA\Server(
url: 'http://localhost',
description: 'Serveur local de développement'
)]
class OpenApiInfo
{
// Classe vide, sert juste à porter l’annotation
}
Puis pour chaque contrôleur, tu ajoutes les annotations correspondantes.
Par exemple, pour le contrôleur GetAllSouverainsController
, tu ajoutes:
use OpenApi\Attributes as OA;
#[OA\Get(
path: '/souverains',
summary: 'Liste tous les souverains',
tags: ['Souverains'],
responses: [
new OA\Response(
response: 200,
description: 'Liste des souverains'
)
]
)]
class GetSouverainsController
{
}
Et pour le contrôleur CreateSouverainController
, tu ajoutes:
use OpenApi\Attributes as OA;
#[OA\Post(
path: '/souverains',
summary: 'Crée un nouveau souverain',
requestBody: new OA\RequestBody(
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
required: ['nom', 'province', 'date_debut'],
properties: [
new OA\Property(property: 'nom', type: 'string'),
new OA\Property(property: 'province', type: 'string'),
new OA\Property(property: 'date_debut', type: 'string', format: 'date'),
new OA\Property(property: 'date_fin', type: 'string', format: 'date'),
new OA\Property(property: 'is_current', type: 'boolean'),
]
)
)
),
tags: ['Souverains'],
responses: [
new OA\Response(
response: 201,
description: 'Souverain créé avec succès'
),
new OA\Response(
response: 400,
description: 'Données incomplètes'
)
]
)]
class CreateSouverainController
{
}
Et pour la GetOneSouverainController
, tu ajoutes:
use OpenApi\Attributes as OA;
#[OA\Get(
path: '/souverains/{id}',
summary: 'Récupère un souverain par son id',
tags: ['Souverains'],
parameters: [
new OA\Parameter(
name: 'id',
description: 'Id du souverain',
in: 'path',
required: true,
schema: new OA\Schema(
type: 'integer'
)
)
],
responses: [
new OA\Response(
response: 200,
description: 'Souverain trouvé'
),
new OA\Response(
response: 404,
description: 'Souverain non trouvé'
)
]
)]
class GetOneSouverainController
{
}
Et c'est partie ouvre le terminal et tape :
vendor/bin/openapi --format yaml -o public/openapi.yaml src/
Cela va générer un fichier openapi.yaml
dans le dossier public/
.
Et si tu ouvre ce fichier tu vas voir que c'est un fichier YAML qui contient la documentation de ton API.
openapi: 3.0.0
info:
title: 'API des souverains'
version: 1.0.1
servers:
-
url: 'http://localhost'
description: 'Serveur local de développement'
paths:
/souverains:
get:
tags:
- Souverains
summary: 'Liste tous les souverains'
operationId: 66bf1ca2b052f8ca961aa85079c3d726
responses:
'200':
description: 'Liste des souverains'
post:
tags:
- Souverains
summary: 'Crée un nouveau souverain'
operationId: adfb775ac40b6388a49722b6f3f80dbf
requestBody:
content:
application/json:
schema:
required:
- nom
- province
- date_debut
properties:
nom:
type: string
province:
type: string
date_debut:
type: string
format: date
date_fin:
type: string
format: date
is_current:
type: boolean
type: object
responses:
'201':
description: 'Souverain créé avec succès'
'400':
description: 'Données incomplètes'
'/souverains/{id}':
get:
tags:
- Souverains
summary: 'Récupère un souverain par son id'
operationId: 09fe39749e06e60a6a807bcb0e103676
parameters:
-
name: id
in: path
description: 'Id du souverain'
required: true
schema:
type: integer
responses:
'200':
description: 'Souverain trouvé'
'404':
description: 'Souverain non trouvé'
/seed:
get:
tags:
- Souverains
summary: 'Peuple la BDD avec des données de test'
operationId: dcbee0864e3cd68bb669a3577d078431
responses:
'200':
description: 'Données de test créées'
tags:
-
name: Souverains
description: Souverains
ET VOILA! Tu as maintenant une documentation de ton API REST en PHP.

Pour aller plus loin
Tu peux maintenant améliorer ton code pour le rendre plus lisible et maintenable.
Astuces:
- Sépare les blocs de requêtes SQL dans des fonctions dédiées; crée un dossier
Service
pour les regrouper. - Évite d’exécuter des requêtes SQL dans une boucle.
- Teste ton code avec des tests unitaires (un article dédié arrive bientôt).
- Essaye de demistifier tout seul le concepte des DTO (Data Transfer Object).
- Et enfin: comment afficher la documentation de ton API directement dans un navigateur?
Conclusion
Le parcours a été long, mais il existe des frameworks comme API Platform (sous Symfony ou Laravel) qui permettent de tout réaliser en 5 minutes .
Mais pour bien utiliser ces outils, il est essentiel d’avoir acquis les bases: les concepts théoriques et le vocabulaire sont indispensables avant d’aborder les frameworks. Comme le disait un ancien, au fond, tout cela reste du PHP.
Maj : 2025-07-12