Sérialiser nos articles
Dans une application créée en mode --webapp, on dispose déjà du serializer de Symfony.
Avant de créer une nouvelle application en mode API, on peut donc déjà commencer à tester le serializer avec nos articles.
Le scénario est tout à fait réaliste : vous pourriez décider, en plus de votre site d'actualités, de mettre à disposition de clients externes un point de terminaison pour récupérer les articles, mais pas via une page web, juste en résultat brut, au format JSON.
Nouveau contrôleur
On crée donc une nouvelle classe de contrôleurs au sein de l'application avec la commande du maker make:controller.
Pour le nom de la classe, si on a déjà un ArticleController dans le dossier src/Controller, on peut préciser, Api\ArticleController. Avec PSR-4, le maker saura où créer la nouvelle classe de contrôleurs, c'est-à-dire dans src/Controller/Api/ArticleController.php.
Envoyer la réponse au format JSON
Dans un contrôleur, on peut utiliser la méthode héritée AbstractController::json pour envoyer le résultat au format JSON :
#[Route('/api/articles', name: 'app_api_articles')]
public function index(ArticleRepository $articleRepository): Response
{
return $this->json($articleRepository->findAll());
}
Le souci, c'est qu'on obtient directement une erreur : A circular reference has been detected when serializing the object of class....
Si on creuse un petit peu, on réalise que le serializer tente de séraliser nos articles, certes, mais tout ce qu'il y a dedans. Et dedans, nous avons entre autres la catégorie.
Mais lorsqu'il explore l'objet Category relié et qu'il tente de le sérialiser, il essaye de sérialiser tout ce qu'il y a dedans, y compris...les articles de cette catégorie. Et ça tombe mal, puisqu'on en vient. C'est pourquoi il lance une erreur de référence circulaire et stoppe tout de suite le processus.
Si nous n'avions aucun système pour stopper ça, alors le serializer bouclerait à l'infini entre un article, sa catégorie, puis les articles de cette catégorie, puis les catégories de ces articles, etc...
Il nous faut un moyen de contrôler la sérialisation.
Contrôler la sérialisation avec les groupes
Pour contrôler la sérialisation, il faut s'interroger sur le format des données qu'on souhaite renvoyer.
En JSON, je souhaite renvoyer un tableau d'articles, en incluant pour chacun leur catégorie pour éviter d'avoir à requêter une seconde fois l'information. Par exemple :
[
{
"id": 1,
"title": "Article title",
"date_created": "dd/mm/yy",
"content": "Content...",
"category": {
"id": 1,
"name": "Category Name"
}
}
//...
]
Avec le serializer de Symfony, je peux déclarer des groupes de sérialisation dans lesquels j'inclurai les propriétés dont j'ai besoin. Pour déclarer qu'une propriété appartient à un groupe, il suffit d'utiliser l'attribut PHP8 Groups du composant serializer :
src/Entity/Article.php
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups('articles:read')] // Nouvel attribut PHP8
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups('articles:read')] // Nouvel attribut PHP8
private ?string $title = null;
//...
#[ORM\ManyToOne(inversedBy: 'articles')]
#[Groups('articles:read')] // Également sur la catégorie
private ?Category $category = null;
Le nom du groupe est totalement arbitraire, mais habituellement on indiquera le nom de l'entité (ou de la ressource, dans le cadre d'une API), puis l'action concernée séparée par deux point :.
On pourra par exemple avoir articles:read, article:read, ou bien articles:item:read
Par la suite, on va indiquer dans l'entité Category les propriétés qu'il faut inclure lorsqu'on demande le groupe articles:read :
src/Entity/Category.php
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups('articles:read')] // Nouvel attribut PHP8
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups('articles:read')] // Nouvel attribut PHP8
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Article::class)]
// Il n'est pas nécessaire d'indiquer qu'on veut sérialiser les articles
// Sinon on déclencherait de nouveau une erreur de référence circulaire
// On peut donc laisser notre propriété comme ça : pas la peine de l'inclure au groupe
private Collection $articles;
Il suffit donc de ne pas inclure les articles dans le groupe concerné par notre opération.
Indiquer le contexte de sérialisation
Enfin, nous pouvons retourner dans notre contrôleur et indiquer à la méthode json le contexte dans lequel on souhaite que la sérialisation s'effectue.
Dans le contexte, de nombreuses options sont disponibles.
L'une d'elles est le groupe que l'on souhaite utiliser. Ainsi, lors de l'appel à la méthode json, on indique au serializer d'utiliser le groupe articles:read :
#[Route('/api/articles', name: 'app_api_articles')]
public function index(ArticleRepository $articleRepository): Response
{
return $this->json(
$articleRepository->findAll(),
context: ['groups' => 'articles:read']
);
}
groupsEn réalité, on voit que l'option s'appelle groups : on pourrait lui indiquer plusieurs groupes, à l'aide d'un tableau
Si on utilise un client comme Postman pour tester notre endpoint, on se retrouve avec quelque chose comme ça :
[
{
"id": 630,
"title": "White Rabbit, who said in a helpless sort of a.",
"date_created": "2021-08-06T00:00:00+00:00",
"content": "Duchess asked, with another dig of her head down to her lips. 'I know SOMETHING interesting is sure to make out which were the cook, to see anything; then she remembered trying to find it out, we should all have our heads cut off, you know. But do cats eat bats? Do cats eat bats, I wonder?' As she said this, she looked at it again: but he would deny it too: but the cook and the pair of gloves and a great crowd assembled about them--all sorts of things, and she, oh! she knows such a thing.",
"category": {
"id": 70,
"name": "sequi"
}
},
{
"id": 631,
"title": "Queen. First came ten soldiers carrying clubs.",
"date_created": "2022-04-26T00:00:00+00:00",
"content": "March Hare and the fan, and skurried away into the garden. Then she went out, but it puzzled her very earnestly, 'Now, Dinah, tell me your history, she do.' 'I'll tell it her,' said the March Hare went 'Sh! sh!' and the little door about fifteen inches high: she tried her best to climb up one of them can explain it,' said the Mouse, who was passing at the Lizard in head downwards, and the procession came opposite to Alice, flinging the baby at her feet, they seemed to Alice an excellent.",
"category": {
"id": 68,
"name": "doloremque"
}
},
{
"id": 632,
"title": "CHAPTER VI. Pig and Pepper For a minute or two.",
"date_created": "2021-07-03T00:00:00+00:00",
"content": "I beg your pardon,' said Alice very meekly: 'I'm growing.' 'You've no right to grow here,' said the Mock Turtle sighed deeply, and began, in a shrill, loud voice, and see that the mouse doesn't get out.\" Only I don't know what it was: she was ever to get through was more and more puzzled, but she did not quite know what they're about!' 'Read them,' said the Pigeon; 'but if you've seen them so often, of course was, how to begin.' He looked anxiously at the bottom of a large caterpillar, that.",
"category": {
"id": 70,
"name": "sequi"
}
}
//...
]
Formater la date
Le format par défaut utilisé pour la date n'est pas convenable. On préfèrerait avoir quelque chose du style dd/mm/yyyy.
Lors de la sérialisation
Toujours dans le contexte, on peut lui indiquer le format de date à utiliser. Pour ce faire, on va utiliser l'option datetime_format également disponible sous forme de constante publique dans la classe DateTimeNormalizer::FORMAT_KEY :
#[Route('/api/articles', name: 'app_api_articles')]
public function index(ArticleRepository $articleRepository): Response
{
return $this->json(
$articleRepository->findAll(),
context: [
'groups' => 'articles:read',
DateTimeNormalizer::FORMAT_KEY => 'd/m/Y'
]
);
}
Le format en sortie est à présent du style : "date_created": "06/08/2021"
Directement sur la propriété
Si on veut éviter de se répéter, on peut également définir une bonne fois pour toutes le format que l'on souhaite observer dans la représentation de nos articles envoyés en JSON par le serializer.
Pour cela, allons à la source : sur la propriété date_created, on peut utiliser l'attribut PHP8 Context pour lui indiquer le paramètre :
#[ORM\Column(type: Types::DATE_MUTABLE)]
#[Groups('articles:read')]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'd/m/Y'])] // Ajout du format
private ?\DateTimeInterface $date_created = null;
Le résultat est le même que précédemment. La différence est simplement qu'on n'aura plus à utiliser ce paramètre de contexte à l'appel du serializer.