Qu’est-ce qu’une API REST et comment renvoyer du JSON en PHP
Une API est une interface par laquelle des programmes échangent des données – une application mobile avec un site, le JavaScript d’une page avec le serveur, ou deux serveurs entre eux. Ce que signifie REST, pourquoi les réponses sont en JSON et comment créer et lire correctement du JSON en PHP.
API et REST
Un site web répond au navigateur avec du HTML, qu’un humain lit. Une API web répond avec des données, qu’un programme lit. Usages typiques :
- une application mobile récupère et enregistre des données sur le serveur ;
- le JavaScript d’une page récupère des données sans la recharger (avec fetch et async/await) ;
- un serveur envoie des données à un autre – par exemple une boutique en ligne au logiciel de gestion des stocks ou de comptabilité.
REST (Representational State Transfer) est une façon de concevoir une telle API. Roy Fielding l’a décrite en 2000 dans sa thèse de doctorat, et elle s’appuie sur ce que HTTP sait déjà faire. En pratique, cela signifie quatre principes :
- Ressources et adresses. Tout ce avec quoi l’API travaille a sa propre adresse : /api/tasks est la liste des tâches, /api/tasks/15 la tâche numéro 15. Les adresses contiennent des noms, pas des verbes.
- Méthodes HTTP. Ce qui doit arriver à la ressource est indiqué par la méthode de la requête : GET lit, POST crée, PATCH modifie, DELETE supprime. En détail dans l’article Méthodes HTTP et routage des requêtes.
- Codes de statut. L’issue de la requête est indiquée par un nombre dans la réponse : 200 tout va bien, 201 créé, 404 introuvable… Voir Codes de statut HTTP et réponses d’erreur.
- Sans état. Chaque requête apporte tout ce dont le serveur a besoin – y compris la clé qui l’autorise. Entre les requêtes, le serveur ne retient rien (aucune session), c’est pourquoi l’API s’appelle facilement depuis n’importe quel programme. Voir Authentification de l’API : clés et jetons.
La différence se voit à la suppression d’une tâche :
- sans REST : GET /api/delete-task.php?id=15 ;
- REST : DELETE /api/tasks/15.
La première variante est en plus dangereuse. Les requêtes GET ne doivent que lire – le navigateur les envoie parfois de lui-même (préchargement des liens), les robots des moteurs de recherche les appellent, et un attaquant peut glisser à n’importe qui un lien vers une telle adresse.
JSON
L’API et son client échangent le plus souvent des données au format JSON (JavaScript Object Notation, norme RFC 8259). C’est un texte qui connaît six sortes de valeurs : un objet entre accolades, un tableau entre crochets, une chaîne de caractères entre guillemets, un nombre, true / false et null :
{
"id": 15,
"title": "Écrire un article sur les API REST",
"done": false,
"tags": ["php", "api"],
"due": null
}
- Une réponse en JSON a l’en-tête Content-Type: application/json. C’est à lui que le client reconnaît ce qu’il a reçu.
- Le JSON échangé entre systèmes doit être encodé en UTF-8. La norme ne définit pas de paramètre charset pour application/json, donc ; charset=utf-8 après le type ne change rien.
- Les clés des objets sont toujours entre guillemets. Les apostrophes, la virgule après le dernier élément et les commentaires ne sont pas autorisés en JSON.
Première réponse en PHP
Une réponse d’API est un script PHP ordinaire qui affiche du JSON au lieu du HTML. La fonction json_encode() convertit un tableau PHP en texte JSON :
Fichier articles.php :
<?php
$articles = array(
array("id" => 1, "title" => "Qu’est-ce qu’une API REST", "tags" => array("api", "json")),
array("id" => 2, "title" => "Codes de statut HTTP", "tags" => array()),
);
header("Content-Type: application/json");
echo json_encode($articles, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
Vous pouvez l’essayer dans le navigateur ou en ligne de commande avec le programme curl. L’option -i affiche aussi la ligne de statut et les en-têtes de la réponse (les résultats de cette série ne montrent que les en-têtes utiles) :
curl -i http://localhost/articles.php
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 1,
"title": "Qu’est-ce qu’une API REST",
"tags": [
"api",
"json"
]
},
{
"id": 2,
"title": "Codes de statut HTTP",
"tags": []
}
]
Les commandes curl de cette série sont écrites pour le shell bash (Linux, macOS, Git Bash sous Windows). Attention à Git Bash sous Windows : son curl convertit le texte des paramètres dans l’ancienne page de code de Windows (p. ex. Windows-1252), les lettres accentuées des données après -d arrivent donc abîmées et l’API répond par l’erreur 400 (Malformed UTF-8). Envoyez ces données depuis un fichier enregistré en UTF-8 (-d @task.json) ou utilisez le curl fourni avec Windows (/c/Windows/System32/curl.exe).
Si vous ne voulez pas tester l’API en ligne de commande, utilisez un programme avec un formulaire de requêtes (Postman, Bruno et autres) ou une extension du navigateur.
json_encode : points d’attention
Sans options, json_encode remplace les lettres accentuées et autres caractères spéciaux par des codes et place une barre oblique inverse devant les barres obliques. Le résultat est du JSON valide et le client le lira de la même façon, il est seulement plus long et moins lisible :
$task = array("id" => 15, "title" => "Café crème", "url" => "/api/tasks/15", "done" => true, "due" => null);
echo json_encode($task), "\n";
echo json_encode($task, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
{"id":15,"title":"Caf\u00e9 cr\u00e8me","url":"\/api\/tasks\/15","done":true,"due":null}
{"id":15,"title":"Café crème","url":"/api/tasks/15","done":true,"due":null}
L’option JSON_PRETTY_PRINT ajoute des indentations et des fins de ligne. Pratique pour le débogage, elle ne fait qu’alourdir la réponse dans une application en production.
Tableau ou objet ? PHP n’a qu’un seul type de tableau, JSON en a deux. json_encode fait d’un tableau aux clés 0, 1, 2… un tableau JSON, et de tout autre tableau un objet. Le piège apparaît après les fonctions qui sautent des clés, comme array_filter() – le client reçoit alors un objet au lieu d’un tableau :
echo json_encode(array("a", "b")), "\n";
echo json_encode(array(1 => "a", 2 => "b")), "\n";
echo json_encode(array_filter(array(3, 0, 5))), "\n";
echo json_encode(array_values(array_filter(array(3, 0, 5)))), "\n";
echo json_encode(array()), " ", json_encode(new stdClass()), " ", json_encode(array(), JSON_FORCE_OBJECT);
["a","b"]
{"1":"a","2":"b"}
{"0":3,"2":5}
[3,5]
[] {} {}
Avant d’afficher une liste, passez-la donc par la fonction array_values(). Un tableau vide est [] en JSON ; si le client doit recevoir un objet vide, utilisez new stdClass() ou l’option JSON_FORCE_OBJECT.
UTF-8 invalide. Un texte mal encodé (par exemple provenant d’une vieille base de données dans une ancienne page de code), json_encode ne le convertit pas et renvoie false – echo affiche alors une chaîne vide et le client reçoit une réponse vide. L’option JSON_THROW_ON_ERROR (depuis PHP 7.3) lève à la place une exception :
var_dump(json_encode("Café \xC3"));
echo json_last_error_msg(), "\n";
try
{
json_encode("Café \xC3", JSON_THROW_ON_ERROR);
}
catch(JsonException $e)
{
echo "Erreur : ", $e->getMessage();
}
bool(false) Malformed UTF-8 characters, possibly incorrectly encoded Erreur : Malformed UTF-8 characters, possibly incorrectly encoded
Sans exception, vous devez vérifier le résultat et trouver l’erreur avec la fonction json_last_error_msg(). Lisez les données de la base en UTF-8 dès la connexion (charset=utf8mb4, voir Connexion à la base de données avec PDO).
Lire le JSON de la requête
Le client envoie les données dans le corps de la requête. Avec du JSON, le tableau $_POST reste pourtant vide – PHP ne le remplit que pour les formulaires (types application/x-www-form-urlencoded et multipart/form-data) et seulement avec la méthode POST. Le corps de la requête se lit avec file_get_contents("php://input"), et json_decode() le convertit en données PHP :
$body = '{"title": "Nouvelle tâche", "done": false, "tags": ["api"]}';
$object = json_decode($body);
$array = json_decode($body, true);
echo $object->title, " | ", $array["title"], " | ", count($array["tags"]), "\n";
var_dump(json_decode('{"title": "Nouvelle tâche",}'), json_decode("null"));
Nouvelle tâche | Nouvelle tâche | 1 NULL NULL
- Sans deuxième paramètre, json_decode renvoie des objets (stdClass), avec true des tableaux.
- En cas d’erreur, il renvoie null – exactement comme pour le JSON valide null. On ne peut donc pas reconnaître ainsi de façon fiable des données invalides du client.
Ici aussi, JSON_THROW_ON_ERROR est fiable :
try
{
$data = json_decode('{"title": "Nouvelle tâche",}', true, 512, JSON_THROW_ON_ERROR);
}
catch(JsonException $e)
{
echo "JSON invalide : ", $e->getMessage();
}
JSON invalide : Syntax error
Le nombre 512 est la profondeur d’imbrication maximale par défaut. Des fonctions prêtes à l’emploi pour la réponse et pour la lecture du corps de la requête, avec les réponses d’erreur, sont décrites dans l’article suivant, Codes de statut HTTP et réponses d’erreur.