• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/13/19

Les services, théorie et création

Log in or subscribe for free to enjoy all this course has to offer!

Vous avez souvent eu besoin d'exécuter une certaine fonction à plusieurs endroits différents dans votre code ? Ou de vérifier une condition sur toutes les pages ? Alors ce chapitre est fait pour vous ! Nous allons découvrir ici une fonctionnalité importante de Symfony : le système de services. Vous le verrez, les services sont utilisés partout dans Symfony, et sont une fonctionnalité incontournable pour commencer à développer sérieusement un site internet sous Symfony.

Ce chapitre ne présente que des notions sur les services, juste ce qu'il vous faut savoir pour les manipuler simplement. Nous verrons dans un prochain chapitre leur utilisation plus poussée.

C'est parti !

Pourquoi utiliser des services ?

Genèse

Vous l'avez vu jusqu'ici, une application PHP, qu'elle soit faite avec Symfony ou non, utilise beaucoup d'objets PHP. Un objet remplit une fonction comme envoyer un e-mail, enregistrer des informations dans une base de données, récupérer le contenu d'un template, etc. Vous pouvez créer vos propres objets qui auront les fonctions que vous leur donnez. Bref, une application est en réalité un moyen de faire travailler tous ces objets ensemble, et de profiter du meilleur de chacun d'entre eux.

Dans bien des cas, un objet a besoin d'un ou plusieurs autres objets pour réaliser sa fonction. Se pose alors la question de savoir comment organiser l'instanciation de tous ces objets. Si chaque objet a besoin d'autres objets, par lequel commencer ?

L'objectif de ce chapitre est de vous présenter le conteneur de services. Chaque objet est défini en tant que service, et le conteneur de services permet d'instancier, d'organiser et de récupérer les nombreux services de votre application. Étant donné que tous les objets fondamentaux de Symfony utilisent le conteneur de services, nous allons apprendre à nous en servir. C'est une des fonctionnalités incontournables de Symfony, et c'est ce qui fait sa très grande flexibilité.

Qu'est-ce qu'un service ?

Un service est simplement un objet PHP qui remplit une fonction, associé à une configuration.

Cette fonction peut être simple : envoyer des e-mails, vérifier qu'un texte n'est pas un spam, etc. Mais elle peut aussi être bien plus complexe : gérer une base de données (le service Doctrine !), etc.

Un service est donc un objet PHP qui a pour vocation d'être accessible depuis n'importe où dans votre code. Pour chaque fonctionnalité dont vous aurez besoin dans toute votre application, vous pourrez créer un ou plusieurs services (et donc une ou plusieurs classes et leur configuration). Il faut vraiment bien comprendre cela : un service est avant tout une simple classe.

Quant à la configuration d'un service, c'est juste un moyen de l'enregistrer dans le conteneur de services. On lui donne un nom, on précise quelle est sa classe, et ainsi le conteneur a la carte d'identité du service.

Prenons pour exemple l'envoi d'e-mails. Dans Symfony il existe le composant SwiftMailer qui permet de gérer les e-mails. Ce composant contient une classe nomméeSwift_Mailer qui envoie effectivement les e-mails. Symfony, qui intègre le composant SwiftMailer, définit déjà cette classe en tant que servicemailer grâce à un peu de configuration. Le conteneur de service de Symfony peut donc accéder à la classeSwift_Mailer  grâce au servicemailer .

Pour ceux qui connaissent, le concept de service est un bon moyen d'éviter d'utiliser trop souvent à mauvais escient le pattern singleton (utiliser une méthode statique pour récupérer l'objet depuis n'importe où).

L'avantage de la programmation orientée services

L'avantage de réfléchir sur les services est que cela force à bien séparer chaque fonctionnalité de l'application. Comme chaque service ne remplit qu'une seule et unique fonction, ils sont facilement réutilisables. Et vous pouvez surtout facilement les développer, les tester et les configurer puisqu'ils sont assez indépendants. Cette façon de programmer est connue sous le nom d'architecture orientée services, et n'est pas spécifique à Symfony ni au PHP.

Le conteneur de services

Mais alors, si un service est juste une classe, pourquoi appeler celle-ci un service ? Et pourquoi utiliser les services ?

L'intérêt réel des services réside dans leur association avec le conteneur de services. Ce conteneur de services (services container en anglais) est une sorte de super-objet qui gère tous les services. Ainsi, pour accéder à un service, il faut passer par le conteneur.

L'intérêt principal du conteneur est d'organiser et d'instancier (créer) vos services très facilement. L'objectif est de simplifier au maximum la récupération des services depuis votre code à vous (depuis le contrôleur ou autre). Vous demandez au conteneur un certain service en l'appelant par son nom, et le conteneur s'occupe de tout pour vous retourner le service demandé.

La figure suivante montre le rôle du conteneur de services et son utilisation. L'exemple est constitué de deux services, sachant que leService1nécessite leService2pour fonctionner, il faut donc qu'il soit instancié après celui-ci.

Fonctionnement du conteneur de services
Fonctionnement du conteneur de services

Vous voyez que le conteneur de services fait un grand travail, mais que son utilisation (ici, depuis le contrôleur) est vraiment simple.

Si l'on devait écrire en PHP ce conteneur de services pour l'exemple, voici ce que cela donnerait (ceci est un code fictif) :

<?php

class Container
{
  protected $service1 = null;
  protected $service2 = null;

  public function getService1()
  {
    if (null !== $this->service1) {
      return $this->service1;
    }

    $service2 = $this->getService2();
    $this->service1 = new Service1($service2);

    return $this->service1;
  }

  public function getService2()
  {
    if (null !== $this->service2) {
      return $this->service2;
    }

    $this->service2 = new Service2();

    return $this->service2;
  }
}

Ce qu'il faut retenir de ce pseudo-code :

  • Lorsque qu'un service a déjà été instancié une fois, le conteneur ne réinstancie pas le service à nouveau : il retourne l'instance précédemment créée ;

  • Lorsqu'on récupère le service1, le conteneur crée le service2 et le passe en argument du service1 (lignes 14-15).

Et si vous êtes d'accord avec moi que l'idée est plutôt simple, j'ai une bonne nouvelle pour vous : c'est exactement comme ça qu'est le vrai conteneur de services de Symfony !

Vérifiez-le par vous-mêmes en ouvrant le fichier var/cache/dev/appDevDebugProjectContainer.php. Ce fichier est très gros, mais regardons la méthode getTemplatingService (trouvez-la avec un CTRL-F ) qui permet de récupérer le moteur de template. Rappelez-vous, on a déjà utilisé le service templating lors du chapitre sur Twig. Et bien, c'est cette méthode que l'on appelait en réalité :

<?php
protected function getTemplatingService()
{
  $this->services['templating'] = $instance = new \Symfony\Bundle\TwigBundle\TwigEngine(
    $this->get('twig'),
    $this->get('templating.name_parser'),
    $this->get('templating.locator')
  );

  return $instance;
}

Je ne vous demande pas de comprendre chaque ligne de ce code, mais juste de saisir l'idée générale. Par exemple, on peut facilement en déduire que les dépendances du service templating sont : twig, templating.name_parser, et templating.locator.

Mais, ce fichier est dans le répertoire de cache. Il est donc écrasé à chaque fois que l'on vide le cache, non ?

Tout à fait ! En effet, le conteneur de services n'est pas figé, il dépend en réalité de votre configuration. Par exemple pour ce service templating, si jamais votre configuration enlève ou ajoute une dépendance, il faut bien que le conteneur de services reflète ce changement : il sera donc regénéré après votre changement.

Comment définir les dépendances entre services ?

Maintenant que vous concevez le fonctionnement du conteneur, il faut passer à la configuration des services. Comment dire au conteneur que leService2doit être instancié avant leService1? Cela se fait grâce à la configuration dans Symfony.

L'idée est de définir pour chaque service :

  • Son nom, qui permettra de l'identifier au sein du conteneur ;

  • Sa classe, qui permettra au conteneur d'instancier le service ;

  • Les arguments dont il a besoin. Un argument peut être un autre service, mais aussi un paramètre (défini dans le fichierparameters.ymlpar exemple).

Nous allons voir la syntaxe de la configuration d'ici peu.

Le partage des services

Il reste un dernier point à savoir avant d'attaquer la pratique. Dans Symfony, chaque service est « partagé ». Cela signifie simplement que la classe du service est instanciée une seule fois (à la première récupération du service) par le conteneur. Si, plus tard dans l'exécution de la page, vous voulez récupérer le même service, c'est cette même instance que le conteneur retournera par la suite.

Ce partage permet de manipuler très facilement les services tout au long de la requête. Concrètement, c'est le même objet$service1 (par exemple) qui sera utilisé dans toute votre application.

Utiliser un service en pratique

Récupérer un service

Continuons sur notre exemple d'e-mail. Comme je vous l'ai mentionné, il existe dans Symfony un composant appelé Swiftmailer, qui permet d'envoyer des e-mails simplement. Il est présent par défaut dans Symfony, sous forme du service Mailer. Ce service est déjà créé, et sa configuration est déjà faite, il ne reste plus qu'à l'utiliser !

Pour accéder à un service déjà enregistré, il suffit d'utiliser la méthodeget($nomDuService)du conteneur. Par exemple :

<?php
$container->get('mailer');

Et comment j'accède à$container, moi ?!

En effet, la question est importante. La réponse n'est pas automatique, mais plutôt quelque chose du genre "ça dépend !". Concentrons-nous pour l'instant sur le cas des contrôleurs, dans lesquels le container est disponible dans l'attribut$container . Depuis un contrôleur, on peut donc écrire ceci :

<?php
$this->container->get('mailer');

Voici en contexte ce que cela donne :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class AdvertController extends Controller
{
  public function indexAction()
  {
    // On a donc accès au conteneur :
    $mailer = $this->container->get('mailer'); 

    // On peut envoyer des e-mails, etc.
  }
}

Et voilà ! Vous savez utiliser le conteneur de services depuis un contrôleur, vraiment très simple comme on l'a vu précédemment.

Créer un service simple

Création de la classe du service

Maintenant que nous savons utiliser un service, apprenons à le créer. Comme un service n'est qu'une classe, il suffit de créer un fichier n'importe où et de créer une classe dedans.

La seule convention à respecter, de façon générale dans Symfony, c'est de mettre notre classe dans un namespace correspondant au dossier où est le fichier. C'est la norme PSR-0 pour l'autoload. Par exemple, la classeOC\PlatformBundle\Antispam\OCAntispam doit se trouver dans le répertoiresrc/OC/PlatformBundle/Antispam/OCAntispam.php. C'est ce que nous faisons depuis le début du cours. :)

Je vous propose, pour suivre notre fil rouge de la plateforme d'annonce, de créer un système anti-spam. Notre besoin : détecter les spams à partir d'un simple texte. Comme c'est une fonction à part entière, et qu'on aura besoin d'elle à plusieurs endroits (pour les annonces et pour les futurs commentaires), faisons en un service. Ce service devra être réutilisable simplement dans d'autres projets Symfony : il ne devra pas être dépendant d'un élément de notre plateforme.

Vous l'aurez compris, ce service étant indépendant de notre plateforme d'annonces, il devrait se trouver dans un bundle séparé ! Un bundle AntiSpamBundle qui offrirait des outils de lutte contre le spam. Toutefois pour rester simple dans notre exemple, plaçons-le quand même dans notreOCPlatformBundle .

Je nommerai ce service « OCAntispam », mais vous pouvez le nommer comme vous le souhaitez. Il n'y a pas de règle précise à ce niveau, mis à part que l'utilisation des underscores (« _ ») est déconseillée.

Je place cette classe dans le répertoire/Antispamde notre bundle, mais vous pouvez à vrai dire faire comme vous le souhaitez.

Créons donc le fichiersrc/OC/PlatformBundle/Antispam/OCAntispam.php, avec ce code pour l'instant :

<?php
// src/OC/PlatformBundle/Antispam/OCAntispam.php

namespace OC\PlatformBundle\Antispam;

class OCAntispam
{
}

C'est tout ce qu'il faut pour avoir un service. Il n'y a vraiment rien d'obligatoire, vous y mettez ce que vous voulez. Pour l'exemple, écrivons un rapide anti-spam : considérons qu'un message est un spam s'il contient moins de 50 caractères (une annonce de mission de moins de 50 caractères n'est pas très sérieuse !). Voici ce que j'obtiens :

<?php
// src/OC/PlatformBundle/Antispam/OCAntispam.php

namespace OC\PlatformBundle\Antispam;

class OCAntispam
{
  /**
   * Vérifie si le texte est un spam ou non
   *
   * @param string $text
   * @return bool
   */
  public function isSpam($text)
  {
    return strlen($text) < 50;
  }
}

La seule méthode publique de cette classe estisSpam(), c'est celle que nous utiliserons par la suite. Elle retournetruesi le message donné en argument (variable$text) est identifié en tant que spam,falsesinon.

Création de la configuration du service

Maintenant que nous avons créé notre classe, il faut la signaler au conteneur de services, c'est ce qui va en faire un service en tant que tel. Un service se définit par sa classe ainsi que sa configuration. Pour cela, nous pouvons utiliser le fichiersrc/OC/PlatformBundle/Ressources/config/services.yml.

Si ce n'est pas le cas, vous devez créer le fichierDependencyInjection/OCPlatformExtension.php(adaptez à votre bundle évidemment). Mettez-y le contenu suivant, qui permet de charger automatiquement le fichierservices.ymlque nous allons modifier :

<?php

namespace OC\PlatformBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

class OCPlatformExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }
}

La méthodeload()de cet objet est automatiquement exécutée par Symfony lorsque le bundle est chargé. Et dans cette méthode, on charge le fichier de configurationservices.yml, ce qui permet d'enregistrer la définition des services qu'il contient dans le conteneur de services. Fin de la parenthèse.

Revenons à notre fichier de configuration. Ouvrez ou créez le fichierRessources/config/services.ymlde votre bundle, et ajoutez-y la configuration pour notre service :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.antispam:
        class: OC\PlatformBundle\Antispam\OCAntispam

Dans cette configuration :

  • oc_platform.antispamest le nom de notre service fraîchement créé. De cette manière, le service sera accessible via$container->get('oc_platform.antispam');. Essayez de respecter la convention en préfixant le nom de vos services par le nom du bundle, ici « oc_platform ».

  • classest un attribut obligatoire de notre configuration, il définit simplement le namespace complet de la classe du service. Cela indique au conteneur de services quelle classe instancier lorsqu'on lui demandera le service.

Et voilà ! Nous avons un service pleinement opérationnel.

Il existe bien sûr d'autres attributs pour affiner la définition de notre service, nous les verrons dans un prochain chapitre sur les services.

Sachez également que le conteneur de Symfony permet de stocker aussi bien des services (des classes) que des paramètres (des variables). Pour définir un paramètre, la technique est la même que pour un service, dans le fichierservices.yml:

parameters:
    mon_parametre: ma_valeur

services:
    # ...

Et pour accéder à ce paramètre, la technique est la même également, sauf qu'il faut utiliser la méthode$container->getParameter('nomParametre');au lieu deget(). C'est d'ailleurs comme cela que vous pouvez récupérer les paramètres qui sont dans le fichierapp/config/parameters.yml  comme les identifiants de votre base de données, etc.

Utilisation du service

Maintenant que notre classe est définie, et notre configuration déclarée, nous avons affaire à un vrai service. Voici un exemple simple de l'utilisation que l'on pourrait en faire :

<?php

// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class AdvertController extends Controller
{
  public function addAction(Request $request)
  {
    // On récupère le service
    $antispam = $this->container->get('oc_platform.antispam');

    // Je pars du principe que $text contient le texte d'un message quelconque
    $text = '...';
    if ($antispam->isSpam($text)) {
      throw new \Exception('Votre message a été détecté comme spam !');
    }
    
    // Ici le message n'est pas un spam
  }
}

Et voilà, vous avez créé et utilisé votre premier service !

Si vous définissez la variable$textavec moins de 50 caractères, vous aurez droit au message d'erreur de la figure suivante.

Mon message était du spam
Mon message était du spam

Créer un service avec des arguments

Passer à la vitesse supérieure

Nous avons un service flambant neuf et opérationnel. Parfait. Mais on n'a pas utilisé toute la puissance du conteneur de services que je vous ai promise : l'utilisation interconnectée des services.

Injecter des arguments dans nos services

En effet, la plupart du temps vos services ne fonctionnent pas seuls, et nécessitent l'utilisation d'autres services, de paramètres ou de variables. Il a donc fallu trouver un moyen propre et efficace pour pallier ce problème, et c'est le conteneur de services qui propose la solution ! Pour passer des arguments à votre service, il faut utiliser sa configuration :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.antispam:
        class: OC\PlatformBundle\Antispam\OCAntispam
        arguments: [] # Tableau d'arguments

Les arguments peuvent être :

  • Des valeurs normales en YAML (des booléens, des chaînes de caractères, des nombres, etc.) ;

  • Des paramètres (définis dans leparameters.ymlpar exemple) : l'identifiant du paramètre est encadré de signes « % » :%nomDuParametre%;

  • Des services : l'identifiant du service est précédé d'une arobase :@nomDuService.

Pour tester l'utilisation de ces trois types d'arguments, je vous propose d'injecter différentes valeurs dans notre service d'antispam, comme ceci par exemple :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.antispam:
        class: OC\PlatformBundle\Antispam\OCAntispam
        arguments:
            - "@mailer"
            - %locale%
            - 50

Dans cet exemple, notre service utilise :

  • @mailer: le service Mailer (pour envoyer des e-mails) ;

  • %locale%: le paramètre locale (pour récupérer la langue, définit dans le fichierapp/config/parameters.yml) ;

  • 50: et le nombre 50 (qu'importe son utilité !).

Une fois vos arguments définis dans la configuration, il vous suffit de les récupérer avec le constructeur du service. Les arguments de la configuration et ceux du constructeur vont donc de paire. Si vous modifiez l'un, n'oubliez pas d'adapter l'autre. Voici donc le constructeur adapté à notre nouvelle configuration :

<?php
// src/OC/PlatformBundle/Antispam/OCAntispam.php

namespace OC\PlatformBundle\Antispam;

class OCAntispam
{
  private $mailer;
  private $locale;
  private $minLength;

  public function __construct(\Swift_Mailer $mailer, $locale, $minLength)
  {
    $this->mailer    = $mailer;
    $this->locale    = $locale;
    $this->minLength = (int) $minLength;
  }

  /**
   * Vérifie si le texte est un spam ou non
   *
   * @param string $text
   * @return bool
   */
  public function isSpam($text)
  {
    return strlen($text) < $this->minLength;
  }
}

L'idée du constructeur est de récupérer les arguments pour les stocker dans les attributs de la classe afin de pouvoir les réutiliser plus tard. L'ordre des arguments du constructeur est le même que l'ordre des arguments définis dans la configuration du service.

Vous pouvez voir que j'ai également modifié la méthodeisSpam()pour vous montrer comment utiliser un argument. Ici, j'ai remplacé le « 50 » que j'avais mis en dur précédemment par la valeur de l'argumentminLength. Ainsi, si vous décidez de passer cette valeur à 100 au lieu de 50, vous ne modifiez que la configuration du service, sans toucher à son code !

L'injection de dépendances

Vous ne vous en êtes pas forcément aperçus, mais on vient de réaliser quelque chose d'assez exceptionnel ! En une seule ligne de configuration, on vient d'injecter un service dans un autre. Ce mécanisme s'appelle l'injection de dépendances (dependency injection en anglais).

L'idée, comme on l'a vu précédemment, c'est que le conteneur de services s'occupe de tout. Votre service a besoin du servicemailer ? Pas de soucis, précisez-le dans sa configuration, et le conteneur de services va prendre soin d'instanciermailer, puis de vous le transmettre à l'instanciation de votre service à vous.

Vous pouvez bien entendu utiliser votre nouveau service dans un prochain service. Au même titre que vous avez mis@mailer  en argument, vous pourrez mettre@oc_platform.antispam  ! ;)

Ainsi retenez bien : lorsque vous développez un service dans lequel vous auriez besoin d'un autre, injectez-le dans les arguments de la configuration, et libérez la puissance de Symfony !

Aperçu du code

Petit aparté sur le code du conteneur de services généré.

Actualisez bien la page pour que Symfony mette à jour son cache, rouvrez le fichiervar/cache/dev/appDevDebugProjectContainer.php  et cherchez la méthode getOcPlatform_AntispamService :

<?php
protected function getOcPlatform_AntispamService()
{
  return $this->services['oc_platform.antispam'] = new \OC\PlatformBundle\Antispam\OCAntispam(
    $this->get('swiftmailer.mailer.default'),
    'en',
    50
  );
}

Vous constatez que Symfony a généré le code nécessaire pour récupérer notre service et ses 3 dépendances. Notez ces deux petits points :

  • Nous avons défini la dépendancemailer mais le conteneur de service crée le serviceswiftmailer.mailer.default. En réalité,mailer est un alias, c'est-à-dire que c'est un pointeur vers un autre service. L'idée est que si un jour vous changez de bibliothèques pour envoyer des e-mails, vous utiliserez toujours le servicemailer dans vos contrôleurs, mais celui-ci pointera vers un autre service ;

  • Le paramètre%locale% que nous avons utilisé a été transformé enen pour english, qui est la valeur que j'ai dans mon fichierparameters.yml. En effet lors de la génération du conteneur de services, Symfony connaît déjà la valeur de ce paramètre, il gagne donc du temps en écrivant directement la valeur et non$this->getParameter('locale').

Pour conclure

Je me permets d'insister sur un point : les services et leur conteneur sont l'élément crucial et inévitable de Symfony. Les services sont utilisés intensément par le cœur même du framework, et nous serons amenés à en créer assez souvent dans la suite de ce cours.

Gardez en tête que leur intérêt principal est de bien découpler les fonctions de votre application. Tout ce que vous comptez utiliser à plusieurs endroits dans votre code mérite un service. Gardez vos contrôleurs les plus simples possibles, et n'hésitez pas à créer des services qui contiennent la logique de votre application. ;)

Ce chapitre vous a donc apporté les connaissances nécessaires pour définir et utiliser simplement les services. Bien sûr, il y a bien d'autres notions à voir, mais nous les verrons un peu plus loin dans un prochain chapitre.

Si vous souhaitez aborder plus en profondeur les notions théoriques abordées dans ce chapitre, je vous propose les lectures suivantes :

En attendant, la prochaine partie abordera la gestion de la base de données !

En résumé

  • Un service est une simple classe associée à une certaine configuration.

  • Le conteneur de services organise et instancie tous vos services, grâce à leur configuration.

  • Les services sont la base de Symfony, et sont très utilisés par le cœur même du framework.

  • L'injection de dépendances est assurée par le conteneur, qui connaît les arguments dont a besoin un service pour fonctionner, et les lui donne donc à sa création.

  • Le code du cours tel qu'il doit être à ce stade est disponible sur la branche iteration-7 du dépot Github.

Example of certificate of achievement
Example of certificate of achievement