20 - Symfony : Les services
Objectifs
- ✅ Comprendre à quoi sert un service.
- ✅ Créer ton propre service.
- ✅ Rendre celui-ci disponible dans ton application.
Pré-requis
Avoir terminé la quête
Introduction
User stories 📋
en tant que visiteur, je souhaite récupérer les données d'une série
en tant que visiteur, je souhaite récupérer la liste des séries
en tant que contributeur, je souhaite ajouter une série
In progress 🤸
en tant que que visiteur, je veux pouvoir accéder à la page d'un épisode via une URL contenant son titre
en tant que que visiteur, je veux pouvoir accéder à la page d'un acteur via une URL contenant son nom
en tant que que visiteur, je veux pouvoir accéder à la page d'une série via une URL contenant son titre
Doing 🏗️
mise en place du service Slugger
Done ✅
en tant que qu'administrateur, je souhaite ajouter des acteurs à une série
création de l'entité Actor
Tu commences à avoir une jolie petite application qui tourne bien. Mais comme pour tout développement, tu peux toujours améliorer et enrichir ton contenu et ses fonctionnalités. Tu as donc encore pas mal de travail. Au programme d'aujourd'hui, tu vas aborder la notion très importante de services dans Symfony.
Sommaire
Qu'est-ce qu'un service ?
Ton projet grandissant va s'étoffer en fonctionnalités. Peut-être voudras-tu mettre en place un mécanisme de suggestion de séries en fonction des goûts de l'utilisateur, récupérer des informations sur les acteurs depuis une API tierce, afficher des statistiques sur le temps de visionnage, etc. Toutes ces fantastiques idées vont nécessiter de nombreuses lignes. Où mettre tout ce code ? Tu aurais tendance à le placer dans tes contrôleurs qui vont alors commencer à enfler de plus en plus. C'est le moment de dire STOP ! Rappel : Le S de SOLID, signifie "Single Responsability". Une classe ou une méthode ne doit avoir qu'une seule responsabilité. La responsabilité d'un contrôleur est de traiter une requête et générer une réponse (le plus souvent une vue Twig). Pour se faire il fait éventuellement appel au modèle. Tout le reste est superflu ! Tout ce code métier n'a donc rien à y faire, sinon tu ne respectes plus le tout premier principe d'un code objet propre. Alors où mettre tout ce code métier supplémentaire ? Tu l'auras compris, dans des services !
Le service en lui-même est simplement l'instance d'une classe remplissant une fonction bien précise (une seule responsabilité) utile à ton application. L'intérêt est également de rendre le code le plus réutilisable possible partout dans ton application. 📖 Petit rappel sur le principe DRY. Les méthodes de tes contrôleurs ne contiendront donc pas directement ce code, mais feront appel aux méthodes des tes services. Le but est simplement de déplacer ton code dans une autre classe pour respecter les principes SOLID.
Au delà des classes de service que tu pourras toi même créer, Symfony propose déjà de nombreux services utilisables dans ton application. Tu peux retrouver la liste de ces services à l'aide de la commande suivante :
symfony console debug:container

Certains noms te semblent familiers ? Comme Twig ou Doctrine ? C'est normal, car sans le savoir, tu les utilises déjà.
Mmmmh, tu as l'air dubitatif. Tu te demandes pourquoi le mot-clé dans la commande est container, alors que depuis le début, on parle de service. Eh bien, c'est parce que ces mêmes services sont centralisés et gérés par le Conteneur de service (service container en anglais).
Injection de dépendances basiques en POO.
Injection de dépendances avancées en POO par le biais de container.
Si tu veux aller plus loin avec le conteneur de service.
Utiliser un service existant
Jusqu'alors, pour accéder à la fiche d'une série, tu passais par l'URL /program/{id}. Cela fonctionne très bien, mais on peut rendre cette URL plus jolie en incluant le titre de la série en question dans l'URL plutôt que son identifiant.
Pourquoi faire cela si ça fonctionne en l'état ? Eh bien, pour une question d'esthétique et de référencement surtout. Du point de vue des moteurs de recherche, l'URL /program/walking-dead aura plus d'importance que /program/1 par exemple. Pour ce faire, il va te falloir générer ce que l'on appelle un slug.
Exemple : le titre d'un article "Découvrir les Services sous Symfony" donnerait le slug "decouvrir-les-services-sous-symfony".
On génère habituellement automatiquement le slug des séries lors de leur création et/ou de leur édition (afin de mettre à jour le permalien).
En pratique, dans une application, si le permalien était amené à être modifié, il faudrait mettre en place une redirection permanente 301 de l'ancienne URL vers la nouvelle afin de ne pas perdre en référencement et d'éviter la présence de pages indexées dans les moteurs de recherche qui seraient des liens morts.
Choisir le service
Symfony dispose d'une grande quantité de services prêts à l'emploi. C'est bien cela d'ailleurs qui rend ce type de framework particulièrement intéressant à utiliser. Mais comment et quel service utiliser ?
Lance dans ta console la commande suivante :
symfony console debug:autowiring slug
Cette commande recherche parmi les services de Symfony ceux contenant le mot clé "slug" et utilisables comme outil dans ton application.
La console te donne ceci en retour :
Autowirable Types
=================
The following classes & interfaces can be used as type-hints when autowiring:
(only showing classes/interfaces matching slug)
Creates a URL-friendly slug from a given string.
Symfony\Component\String\Slugger\SluggerInterface (slugger)
Tu apprends ici qu'un objet du type SluggerInterface peut être injecté en dépendance dans ton application partout où tu en as besoin. Voyons cela en détail.
Rends-toi au niveau de la méthode du contrôleur qui te permet d’ajouter une série en base de données. 💻 Commence par rajouter un argument à cette méthode :
// src/Controller/ProgramController.php
public function new(..., SluggerInterface $slugger)
Assure-toi que le
use Symfony\Component\String\Slugger\SluggerInterfaceest bien là.
Grâce aux injections de dépendances, tu n'as pas besoin d'instancier toi-même l'objet dans le corps de ta méthode.
💻 La documentation de ce service nous indique qu'une méthode slug() est disponible et accepte en paramètre la chaîne de caractère à sluggifier. Utilise cette méthode et passe-lui donc en argument le titre de la série. Le tout, avant la persistence de ton nouvel objet Program.
$slug = $slugger->slug($program->getTitle());
$program->setSlug($slug);
Deux mots clés à retenir dans ce que nous venons de voir :
-
le TypeHinting : préciser dans les paramètres de la méthode que
$sluggerdoit être un objet de la classeSluggerInterface, -
l'Autowiring (câblage automatique) : instanciation automatique de la part du framework de cet objet
SluggerInterface.
Ainsi, le service sera sollicité à chaque ajout d'une nouvelle série.
Mais, attends une minute ! Tu appelles une méthode setSlug() sur un objet qui ne dispose pas de cette fonctionnalité, ni de la propriété slug ?!? Eh bien, tu as tout compris. Tu vas devoir ajouter cet attribut à ton entité Program. Utilise les commandes suivantes :
-
symfony console make:entityafin de rajouter la propriété slug, qui doit contenir un string ne pouvant pas êtrenull, -
symfony console make:migrationpour créer la classe migration, -
symfony console doctrine:migrations:migratepour impacter la DB avec ce nouveau champ.
Une fois ces différentes étapes effectuées, l'utilisation du service prendra tout son sens.
Appeler un service en dehors d'un contrôleur
Dans la partie précédente, tu as vu comment utiliser un service depuis un contrôleur. Cependant, il arrive fréquemment que tu aies besoin d'utiliser un service depuis une autre classe (dans un autre service, dans les fixtures, etc.)
Dans ce cas, la syntaxe est un peu différente car tu devras utiliser le mécanisme d'injection de dépendance dans le __construct() (et non directement dans les méthodes comme pour les contrôleurs).
Dans tes fixtures par exemple, tu vas vouloir ajouter le slug et donc générer automatiquement ce dernier en fonction du title de la série.
Réfère toi à la documentation suivante et adapte ton code pour y appeler ce service SluggerInterface.
Créer son propre service
Tu vas maintenant passer à la création de ton propre service. Voici l'idée :
- Tu vas ajouter la durée d'un épisode, puis créer un service qui permettra de calculer automatiquement la durée totale d'une série.
Préparation des contenus
- Utilise les mêmes commandes (make:entity, make:migration, etc.) que tu as lancées pour la création de la propriété slug mais cette fois-ci pour créer la propriété
durationsur l'entitéEpisode. Elle sera du type integer car tu vas renseigner ces durées en minutes. - Modifie tes fixtures en ajoutant des valeurs sur cette nouvelle propriété.
Le service
-
À la racine de ton répertoire src/, crée un nouveau répertoire "Service".
-
Crée une nouvelle classe "ProgramDuration" avec le bon namespace (en accord avec l'arborescence fichiers) et sa structure de base. Tu sais faire ça tout.e seul.e maintenant !
bashsrc/ ⎿ Service/ ⎿ ProgramDuration.php -
Enfin, déclare la méthode qui va te permettre de calculer la durée totale d'une série
calculate(Program $program): string. Pour l'instant, la méthodecalculate(): stringretournera "coming soon", tu t'occuperas du calcul plus tard...
Ton service est maintenant prêt à être appelé là où tu en as besoin. Rends-toi au niveau de la méthode du contrôleur qui te permet d'afficher une série. Ajoute un argument à cette méthode :
// src/Controller/ProgramController.php
public function show(Program $program, ..., ProgramDuration $programDuration)
Assure-toi que le
useest bien là.
Tu peux ensuite ajouter au contexte passé à ta vue, l'appel à la méthode calculate() de ton service pour pouvoir afficher la durée sur ton template.
// src/Controller/ProgramController.php
public function show(Program $program, ..., ProgramDuration $programDuration)
{
return $this->render('program/show.html.twig', [
...,
'programDuration' => $programDuration->calculate($program)
]);
}
💪 Challenge
Appeler la génération du Slug
Dans ce challenge, tu dois faire en sorte que le service SluggerInterface soit utilisé à toutes les étapes du cycle de vie de tes entités, pour cela, il faut bien évidemment ajouter un champ slug sur les entités qui le nécessitent :
- Program :
- Lors de l'ajout d'une série depuis un formulaire,
- Lors de l'ajout d'une série depuis les fixtures (injecter le service via le
__construct()), - Lors de l'édition d'une série.
- Episode :
- Lors de l'ajout d'un épisode depuis un formulaire,
- Lors de l'ajout d'un épisode depuis les fixtures (injecter le service via le
__construct()), - Lors de l'édition d'un épisode.
Une fois tes slugs générés, modifie tes routes de contrôleurs afin qu'elles n'utilisent plus l'id de la ressource pour y accéder mais le slug (grâce au ParamConverter).
- Par exemple :
GET /programs/walking-dead: affiche la fiche de la série Walking DeadGET|POST /episode/days-gone-by/edit: édition de l'épisode 1 de la saison 1 de Walking Dead- ...etc
BONUS : Afficher la durée d'une série
- Écris le corps de la méthode
calculate()du serviceProgramDurationpour qu'elle renvoie le nombre d'heures (voire de jours) et de minutes nécessaires au visionnage complet d'une série. (ex : pour une durée totale de 1610 minutes : 1 jour 2 heures et 50 minutes). - Affiche la durée sur le template
program/show.html.twig.
Critères de validation
- Les entités Program, Episode comportent un champ slug
- Les routes des différents contrôleurs relatifs aux séries et épisodes utilisent les slugs plutôt que les id.
- Le service SluggerInterface est appelé à chaque niveau de l’application où il y a un ajout de série.
- Le service SluggerInterface est appelé à chaque niveau de l’application où il y a un ajout/modification d’un épisode.
- BONUS : Le service
ProgramDurationcréé auparavant avec une méthodecalculate()permet de calculer la durée de visionnage totale d'une série. - Le code est disponible sur un repository GitHub, avec une branche correspondant à cette quête.
Ressources partagées sur cette quête
- Ne vous répétez pas (version française)https://fr.wikipedia.org/wiki/Ne_vous_r%C3%A9p%C3%A9tez_pas
- Bonne documentation de slughttps://symfonycasts.com/screencast/symfony-doctrine/sluggable