Je prépare actuellement la conférence que je donnerai à l’AFUP Day au mois de mai, et dont le titre sera “Repenser les APIs”. Dans les différents points que je vais aborder, il y a la “négociation de contenu”, qu’on appelle en anglais “content negociation”, “dual-purpose endpoint” ou encore “API mode switch”.
Le principe
L’idée de la négociation de contenu, c’est de proposer une API sans avoir besoin d’en développer une. Prenons un exemple classique : vous avez un site média ou e-commerce, avec des pages qui présentent des listes d’articles, et des pages qui présentent les articles eux-mêmes (articles à lire pour un site média, articles à acheter pour un site e-commerce).
À côté du site, vous avez une application mobile, qui permet d’accéder aux mêmes contenus. Cette application a besoin de se connecter à une API pour récupérer les listes d’articles et la définition complète de chaque article, pour les afficher comme elle le souhaite.
En temps normal, on créerait donc une API, à laquelle l’appli se connecterait, qui retournerait des flux JSON. Mais il est beaucoup plus simple de demander aux URLs du site de retourner du JSON et non pas du HTML. Ainsi, la page qui liste des articles retournerait une liste de tableaux associatifs, chacun contenant les quelques informations nécessaires pour afficher la liste (nom de l’article, identifiant, auteur, date de création, prix, etc.). La page qui affiche habituellement un article retournerait un flux JSON contenant toutes les informations sur un article.
Mise en œuvre
L’avantage de cette solution, c’est évidemment que la quasi-totalité du code est déjà là. Tout ce qu’il reste à faire, c’est de prendre les données qui sont normalement passées au template (pour générer du HTML), et les rediriger vers une vue qui va les envoyer au format JSON. Et. C’est. Tout.
Exemple de code Symfony
Fichier src/Controller/ArticleController.php :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
class ArticleController extends AbstractController
{
#[Route('/article/message', requirements: ['_format' => 'html|json'])]
public function messageAction(Request $request)
{
$data = ['message' => 'Hello World'];
if ($request->getPreferredFormat() === 'json') {
return new JsonResponse($data);
}
return $this->render(
'page.html.twig',
$data
);
}
}
- Ligne 10 : On définit la route, en indiquant que le format demandé par le client peut être du HTML ou du JSON.
- Ligne 13 : On prépare les données qui seront envoyées au template (pour générer du HTML) ou qui seront sérialisées en JSON.
- Lignes 14 et 15 : Si le client veut recevoir du JSON, on utilise l’objet
JsonResponsepour sérialiser les données. - Lignes 17 à 20 : Sinon, on fait un rendu HTML en utilisant un template Twig.
Exemple de code Laravel
On commence dans la définition de la route (fichier routes/web.php) :
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
Route::get('/article/message', [ArticleController::class, 'messageAction']);
Contrairement à Symfony, on n’a pas besoin de déclarer dans la route que l’action accepte d’envoyer du JSON.
Et le code de l’action (fichier app/Http/Controllers/ArticleController.php) :
namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use Illuminate\Http\Request;
class ArticleController extends Controller {
public function messageAction(Request $request) {
$data = ['message' => 'Hello World'];
if ($request->expectsJson()) {
return response()->json($data);
}
return view('article', $data);
}
}
- Ligne 8 : On prépare les données qui seront envoyées au template (pour générer du HTML) ou qui seront sérialisées en JSON.
- Lignes 9 et 10 : Si le client veut recevoir du JSON, on appelle la méthode
json()de l’objetResponsepour sérialiser les données en sortie. - Ligne 12 : Sinon, on fait un rendu HTML en utilisant un template Blade.
Exemple de code Temma
Fichier controllers/Article.php :
use \Temma\Attributes\View as TµView;
class Article extends \Temma\Web\Controller {
#[TµView(negotiation: 'html, json')]
public function message() {
$this['message'] = 'Hello World';
}
}
- Ligne 4 : On utilise l’attribut View pour que la vue s’adapte en fonction de ce que demande le client, en autorisant uniquement le HTML (vue Smarty) ou le JSON (vue Json).
- Ligne 6 : On crée la variable de template, qui sera passée soit à la vue
Smarty, soit à la vueJson. Si c’est la vueSmartyqui est exécutée, elle utilisera par défaut le templatetemplates/article/monAction.tpl.
Dans Temma, l’approche est différente de Symfony et Laravel : l’action n’a pas à se soucier de regarder si on lui demande du HTML ou du JSON, son code est toujours le même.
On utilise un attribut pour définir dynamiquement la vue à utiliser, en fonction du type de contenu demandé par le client. Ici, l’attribut a été placé sur l’action, mais il pourrait être placé sur le contrôleur, et dans ce cas le traitement serait effectif sur toutes les actions.
