Licence CC BY-NC-SA

Les services, utilisation poussée

Publié :
Auteur :
Catégorie :

Ce chapitre fait suite au précédent chapitre sur les services, qui portait sur la théorie et l'utilisation simple.

Ici nous allons aborder des fonctionnalités intéressantes des services, qui permettent une utilisation vraiment poussée. Maintenant que les bases vous sont acquises, nous allons pouvoir découvrir des fonctionnalités très puissantes de Symfony.

Les tags sur les services

Les tags ?

Une fonctionnalité très importante des services est la possibilité d'utiliser les tags. Un tag est une option qu'on appose à un ou plusieurs services afin que le conteneur de services les identifie comme tels. Ainsi, il devient possible de récupérer tous les services qui possèdent un certain tag.

C'est un mécanisme très pratique pour ajouter des fonctionnalités à un composant. Pour bien comprendre, prenons l'exemple de Twig.

Comprendre les tags à travers Twig

Le moteur de templates Twig dispose nativement de plusieurs fonctions pratiques pour vos vues. Seulement, il serait intéressant de pouvoir ajouter nos propres fonctions qu'on pourra utiliser dans nos vues. Vous l'aurez compris, c'est possible grâce au mécanisme des tags.

Appliquer un tag à un service

Pour que Twig puisse récupérer tous les services qui vont définir des fonctions supplémentaires utilisables dans nos vues, il faut appliquer un tag à ces services.

Si vous avez toujours le service SdzAntispam défini au précédent chapitre sur les services, je vous invite à rajouter le tag dans la configuration du service. Bien entendu, si vous n'aviez pas ce service, rien ne vous empêche d'en créer un nouveau : vous taguez les services que vous voulez.

1
2
3
4
5
6
7
8
# src/Sdz/BlogBundle/Resources/config/services.yml

services:
    sdz_blog.antispam:
        class:     Sdz\BlogBundle\Antispam\SdzAntispam
        arguments: [@mailer, %locale%, 3]
        tags:
            -  { name: twig.extension }

Vous voyez, on a simplement rajouté un attribut tags à la configuration de notre service. Cet attribut contient un tableau de tags, d'où le retour à la ligne et le tiret « - ». En effet, il est tout à fait possible d'associer plusieurs tags à un même service.

Dans ce tableau de tags, on a ajouté une ligne avec un attribut name, qui est le nom du tag. C'est grâce à ce nom que Twig va pouvoir récupérer tous les services avec ce tag. Ici twig.extension est donc le tag qu'on utilise.

Bien entendu, maintenant que Twig peut récupérer tous les services tagués, il faut que tout le monde puisse se comprendre.

Une classe qui implémente une interface

Celui qui va récupérer les services d'un certain tag attend un certain comportement de la part des services qui ont ce tag. Il faut donc les faire implémenter une interface ou étendre une classe de base.

En particulier, Twig attend que votre service implémente l'interface Twig_ExtensionInterface. Encore plus simple, Twig propose une classe abstraite à hériter par notre service, il s'agit de Twig_Extension. Je vous invite donc à modifier notre classe SdzAntispam pour qu'elle hérite de Twig_Extension (qui, elle, implémente Twig_ExtensionInterface) :

1
2
3
4
5
6
7
8
9
<?php
// src/Sdz/BlogBundle/Antispam/SdzAntispam.php

namespace Sdz\BlogBundle\Antispam;

class SdzAntispam extends \Twig_Extension
{
  // …
}

Notre service est prêt à fonctionner avec Twig, il ne reste plus qu'à écrire au moins une des méthodes de la classe abstraite Twig_Extension.

Écrire le code qui sera exécuté

Cette section est propre à chaque tag, où celui qui récupère les services d'un certain tag va exécuter telle ou telle méthode des services. En l'occurrence, Twig va exécuter les méthodes suivantes :

  • getFilters(), qui retourne un tableau contenant les filtres que le service ajoute à Twig ;
  • getTests(), qui retourne les tests ;
  • getFunctions(), qui retourne les fonctions ;
  • getOperators(), qui retourne les opérateurs ;
  • getGlobals(), qui retourne les variables globales.

Pour notre exemple, nous allons juste ajouter une fonction accessible dans nos vues via {{ checkIfSpam('le message') }}. Elle vérifie si son argument est un spam. Pour cela, écrivons la méthode getFunctions() suivante dans notre service :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
// src/Sdz/BlogBundle/Antispam/SdzAntispam.php

namespace Sdz\BlogBundle\Antispam;

class SdzAntispam extends \Twig_Extension
{
  /*
   * Twig va exécuter cette méthode pour savoir quelle(s) fonction(s) ajoute notre service
   */
  public function getFunctions()
  {
    return array(
      'checkIfSpam' => new \Twig_Function_Method($this, 'isSpam')
    );
  }

  /*
   * La méthode getName() identifie votre extension Twig, elle est obligatoire
   */
  public function getName()
  {
    return 'SdzAntispam';
  }

  // …
}

Dans cette méthode getFunctions() :

  • checkIfSpam est le nom de la fonction qui sera disponible sous Twig, via {{ checkIfSpam(var) }}.
  • new \Twig_Function_Method($this, 'isSpam') est ce qui sera exécuté lors de l'appel à la fonction Twig checkIfSpam(). Ici, il s'agit de la méthode isSpam() de l'objet courant $this.
  • Au final, {{ checkIfSpam(var) }} côté Twig exécute $this->isSpam($var) côté service.

On a également ajouté la méthode getName() qui identifie votre service de manière unique parmi les extensions Twig, elle est obligatoire, ne l'oubliez pas.

Et voilà ! Vous pouvez dès à présent utiliser la fonction {{ checkIfSpam() }} dans vos vues. ;)

Méthodologie

Ce qu'on vient de faire pour transformer notre simple service en extension Twig est la méthodologie à appliquer systématiquement lorsque vous taguez un service. Sachez que tous les tags ne nécessitent pas forcément que votre service implémente une certaine interface, mais c'est assez fréquent.

Pour connaître tous les services implémentant un certain tag, vous pouvez exécuter la commande suivante :

1
2
3
4
5
6
C:\wamp\www\Symfony> php app/console container:debug --tag=twig.extension

[container] Public services with tag twig.extension
Service Id          Scope     Class Name
sdz_blog.antispam   container Sdz\BlogBundle\Antispam\SdzAntispam
twig.extension.intl container Twig_Extensions_Extension_Intl

Vous trouverez plus d'informations sur la création d'extensions Twig dans la documentation de Twig.

Les principaux tags

Il existe pas mal de tags prédéfinis dans Symfony2, qui permettent d'ajouter des fonctionnalités à droite et à gauche. Je ne vais vous présenter ici que deux des principaux tags. Mais sachez que l'ensemble des tags est expliqué dans la documentation.

Les évènements du cœur

Les services peuvent être utilisés avec le gestionnaire d'évènements, via plusieurs tags. Dans ce cas, les différents tags permettent d'exécuter un service à des moments précis dans l'exécution d'une page. Le gestionnaire d'évènements est un composant très intéressant, et fait l'objet d'un prochain chapitre dédié.

Les types de champ de formulaire

Le tag form.type permet de définir un nouveau type de champ de formulaire. Par exemple, si vous souhaitez utiliser l'éditeur WYSIWYG (What you see is what you get) ckeditor pour certains de vos champs texte, il est facile de créer un champ ckeditor au lieu de textarea. Pour cela, disons que vous avez ajouté le JavaScript nécessaire pour activer cet éditeur sur les <textarea> qui possèdent la classe ckeditor. Il ne reste plus qu'à automatiser l'apparition de cette classe.

Commençons par créer la classe du type de champ :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
// src/Sdz/BlogBundle/Form/Type/CkeditorType.php

namespace Sdz\BlogBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CkeditorType extends AbstractType
{
  public function setDefaultOptions(OptionsResolverInterface $resolver)
  {
    $resolver->setDefaults(array(
      'attr' => array('class' => 'ckeditor')
    ));
  }

  public function getParent()
  {
    return 'textarea';
  }

  public function getName()
  {
    return 'ckeditor';
  }
}

Ce type de champ hérite de toutes les fonctionnalités d'un textarea (grâce à la méthode getParent()) tout en disposant de la classe CSS ckeditor (définie dans la méthode setDefaultOptions()) vous permettant, en ajoutant ckeditor à votre site, de transformer vos <textarea> en éditeur WYSIWYG.

Puis, déclarons cette classe en tant que service, en lui ajoutant le tag form.type :

1
2
3
4
5
6
7
# src/Sdz/BlogBundle/Resources/config/services.yml

services:
    sdz_blog.ckeditor:
        class: Sdz\BlogBundle\Form\Type\CkeditorType
        tags:
            - { name: form.type, alias: ckeditor }

On a ajouté l'attribut alias dans le tag, qui représente le nom sous lequel on pourra utiliser ce nouveau type. Pour l'utiliser, c'est très simple, modifiez vos formulaires pour utiliser ckeditor à la place de textarea. Par exemple, dans notre ArticleType :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ArticleType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
      // …
      ->add('contenu', 'ckeditor')
    ;
  }

  // …
}

Et voilà, votre champ a maintenant automatiquement la classe CSS ckeditor, ce qui permet d'activer l'éditeur (si vous l'avez ajouté à votre site bien sûr).

C'était un exemple pour vous montrer comment utiliser les tags dans ce contexte.

Pour plus d'informations sur la création de type de champ, je vous invite à lire la documentation à ce sujet.

Dépendances optionnelles : les calls

Les dépendances optionnelles

L'injection de dépendances dans le constructeur, comme on l'a fait dans le précédent chapitre sur les services, est un très bon moyen de s'assurer que la dépendance sera bien disponible. Mais parfois vous pouvez avoir des dépendances optionnelles. Ce sont des dépendances qui peuvent être rajoutées au milieu de l'exécution de la page, grâce à des setters. Reprenons par exemple notre service d'antispam, où l'on définit l'argument $locale comme optionnel. L'idée est de supprimer ce dernier des arguments du constructeur, et d'ajouter le setter correspondant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
// src/Sdz/BlogBundle/Antispam/SdzAntispam.php

namespace Sdz\BlogBundle\Antispam;

class SdzAntispam extends \Twig_Extension
{
  protected $mailer;
  protected $locale;
  protected $nbForSpam;

  // Dans le constructeur, on retire $locale des arguments
  public function __construct(\Swift_Mailer $mailer, $nbForSpam)
  {
    $this->mailer    = $mailer;
    $this->nbForSpam = (int) $nbForSpam;
  }

  // Et on ajoute un setter
  public function setLocale($locale)
  {
    $this->locale = $locale;
  }

  // …
}

N'oubliez pas de supprimer l'argument %locale% de la définition du service. Rappelez-vous : si vous modifiez le constructeur du service, vous devez adapter sa configuration, et inversement.

Cependant, dans ce cas, la dépendance optionnelle $locale n'est jamais renseignée par le conteneur, pas même avec une valeur par défaut. C'est ici que les calls interviennent.

Les calls

Les calls sont un moyen d'exécuter des méthodes de votre service juste après sa création. Ainsi, on peut exécuter la méthode setLocale() avec notre paramètre %locale%, qui sera une valeur par défaut pour ce service. Elle pourra tout à fait être écrasée par une autre au cours de l'exécution de la page.

Je vous invite donc à rajouter l'attribut calls à la définition du service, comme ceci :

1
2
3
4
5
6
7
8
# src/Sdz/BlogBundle/Resources/config/services.yml

services:
    sdz_blog.antispam:
        class:     Sdz\BlogBundle\Antispam\SdzAntispam
        arguments: [@doctrine, 3]
        calls:
            - [ setLocale, [ %locale% ] ]

Comme avec les tags, vous pouvez définir plusieurs calls, en rajoutant des lignes à tiret. Chaque ligne de call est un tableau qui se décompose comme suit :

  • Le premier index, ici setLocale, est le nom de la méthode à exécuter ;
  • Le deuxième index, ici [ %locale% ], est le tableau des arguments à transmettre à la méthode exécutée. Ici nous avons un seul argument, mais il est tout à fait possible d'en définir plusieurs.

Concrètement, dans notre cas le code équivalent du conteneur serait celui-ci :

1
2
3
4
<?php

$antispam = new \Sdz\BlogBundle\Antispam\SdzAntispam($doctrine, 3);
$antispam->setLocale($locale);

Ce code n'existe pas, c'est un code fictif pour vous représenter l'équivalent de ce que fait le conteneur de services dans son coin. ;)

L'utilité des calls

En plus du principe de dépendance optionnelle, l'utilité des calls est également remarquable pour l'intégration des bibliothèques externes (Zend Framework, GeSHI, etc.), qui ont besoin d'exécuter quelques méthodes en plus du constructeur. Vous pourrez donc le faire grâce aux calls.

Les champs d'application, ou scopes

Cette section traite des champs d'application des services, scopes en anglais. C'est une notion avancée et il faut que vous sachiez qu'elle existe. En effet, vous rencontrerez sûrement un jour une erreur mentionnant le scope d'un de vos services. Voici les clés pour vous en sortir.

La problématique

Pour bien comprendre la problématique posée par le scope d'un service, je vous propose un exemple simple.

Prenez notre service Antispam, pour lequel nous n'avons pas précisé de scope particulier. Le comportement par défaut du conteneur, comme je vous l'ai expliqué, est qu'à chaque fois que vous appelez le service Antispam, vous recevez le même objet. Il est en effet stocké dans le conteneur au premier appel, puis c'est le même objet qui vous est retourné toutes les fois suivantes.

Imaginons maintenant qu'on injecte la requête, le service request, dans notre service Antispam. On s'en sert pour récupérer l'URL courante par exemple, et faire un comportement différent selon sa valeur.

Voyons alors quelles sont les conséquences dans l'utilisation pratique de notre service Antispam :

  1. Vous appelez le service Antispam. Une instance de la classe est donc créée, appelons-la Antispam1, avec comme argument une instance de la classe Request, appelons-la Request1.
  2. Maintenant, vous effectuez une sous-requête, un {% render %} depuis une vue par exemple. C'est une sous-requête, donc le noyau de Symfony2 va créer une nouvelle requête, une nouvelle instance de Request, appelons-la Request2.
  3. Dans cette sous-requête, vous faites encore appel au service Antispam. Rappelez-vous, c'est la même instance qui vous est retournée à chaque fois, vous utilisez donc à nouveau Antispam1. Or, cette instance Antispam1 contient l'objet Request1 et non Request2 ! Du coup, l'URL que vous utilisiez dans le service est ici erronée, puisque la requête a été changée entre temps !

Les scopes sont justement là pour éviter ce genre de problème. En effet, il est en réalité impossible d'être confronté à l'exemple que nous venons de voir, car le conteneur vous retournera une erreur si vous essayez d'injecter la requête dans un service dont le scope n'est pas défini.

Que sont les scopes ?

Le scope d'un service sert de cadre pour les interactions entre une instance de ce service et le conteneur de services. Le conteneur de Symfony2 offre trois scopes par défaut :

  • container (la valeur par défaut) : le conteneur vous retourne la même instance du service à chaque fois que vous lui demandez ce service. C'est le comportement par défaut dont j'ai toujours parlé jusqu'à maintenant.
  • request : le conteneur vous retourne la même instance du service au sein de la même requête. En cas de sous-requête, le conteneur crée une nouvelle instance du service et vous retourne la nouvelle, l'ancienne n'est plus accessible.
  • prototype : le conteneur vous retourne une nouvelle instance à chaque fois que vous lui demandez ce service.

Les scopes induisent donc une contrainte sur les dépendances d'un service (ce que vous injectez dans ce service) : un service ne peut pas dépendre d'un autre service au scope plus restreint. Le scope prototype est plus restreint que request, qui lui-même est plus restreint que container.

Pour reprendre l'exemple précédent, si vous avez un service SdzAntispam générique (c'est-à-dire avec le scope container par défaut), mais que vous essayez de lui injecter le service request qui est de scope plus restreint, vous recevrez une erreur (ScopeWideningInjectionException) au moment de la compilation du conteneur.

Et concrètement ?

En pratique, vous rencontrerez cette contrainte sur les scopes à chaque fois que vous injecterez la requête dans un de vos services. Pensez donc, à chaque fois que vous le faites, à définir le scope à request dans la configuration de votre service :

1
2
3
4
5
6
7
# src/Sdz/BlogBundle/Resources/config/services.yml

services:
    sdz_blog.antispam:
        class:     Sdz\BlogBundle\Antispam\SdzAntispam
        arguments: [@request, @unAutreService]
        scope:     request

Et bien entendu, maintenant que ce service a le scope request, vous devrez appliquer ce même scope à tous les autres services dans lesquels vous l'injecterez.

Les services courants de Symfony2

Les services courants de Symfony

Maintenant que vous savez bien utiliser les services, il vous faut les connaître tous afin d'utiliser la puissance de chacun. Il est important de bien savoir quels sont les services existants afin de bien pouvoir injecter ceux qu'il faut dans les services que vous êtes amenés à créer de votre côté.

Je vous propose donc une liste des services par défaut de Symfony2 les plus utilisés. Gardez-la en tête !

Identifiant

Description

doctrine.orm.entity_manager

Ce service est l'instance de l'EntityManager de Doctrine ORM. On l'a rapidement évoqué dans la partie sur Doctrine, l'EntityManager est bien enregistré en tant que service dans Symfony2. Ainsi, lorsque dans un contrôleur vous faites $this->getDoctrine()->getManager(), vous récupérez en réalité le service doctrine.orm.entity_manager. Ayez bien ce nom en tête, car vous aurez très souvent besoin de l'injecter dans vos propres services : il vous offre l'accès à la base de données, ce n'est pas rien !

event_dispatcher

Ce service donne accès au gestionnaire d'évènements. Le décrire en quelques lignes serait trop réducteur, je vous propose donc d'être patients, car le prochain chapitre lui est entièrement dédié. ;)

kernel

Ce service vous donne accès au noyau de Symfony. Grâce à lui, vous pouvez localiser des bundles, récupérer le chemin de base du site, etc. Voyez le fichier Kernel.php pour connaître toutes les possibilités. Nous nous en servirons très peu en réalité.

logger

Ce service gère les logs de votre application. Grâce à lui, vous pouvez utiliser des fichiers de logs très simplement. Symfony utilise la classe Monolog par défaut pour gérer ses logs. La documentation à ce sujet vous expliquera comment vous en servir si vous avez besoin d'enregistrer des logs pour votre propre application ; c'est intéressant, n'hésitez pas à vous renseigner.

mailer

Ce service vous renvoie par défaut une instance de Swift_Mailer, une classe permettant d'envoyer des e-mails facilement. Encore une fois, la documentation de SwiftMailer et la documentation de son intégration dans Symfony2 vous seront d'une grande aide si vous souhaitez envoyer des e-mails.

request

Ce service est très important : il vous donne une instance de Request qui représente la requête du client. Rappelez-vous, on l'a déjà utilisé en récupérant cette dernière directement via $this->getRequest() depuis un contrôleur. Je vous réfère au chapitre sur les contrôleurs, section Request pour plus d'informations sur comment récupérer la session, l'IP du visiteur, la méthode de la requête, etc.

router

Ce service vous donne accès au routeur (Symfony\Component\Routing\Router). On l'a déjà abordé dans le chapitre sur les routes. Rappelez-vous, on générait des routes à l'aide du raccourci du contrôleur $this->generateUrl(). Sachez aujourd'hui que cette méthode exécute en réalité $this->container->get('router')->generate(). On utilisait déjà des services sans le savoir !

security.context

Ce service permet de gérer l'authentification sur votre site internet. On l'utilise notamment pour récupérer l'utilisateur courant. Le raccourci du contrôleur $this->getUser() exécute en réalité $this->container->get('security.context')->getToken()->getUser() !

service_container

Ce service vous renvoie le conteneur de services lui-même. On ne l'utilise que très rarement, car, comme je vous l'ai déjà mentionné, il est bien plus propre de n'injecter que les services dont on a besoin, et non pas tout le conteneur. Mais dans certains cas il est nécessaire de s'en servir, sachez donc qu'il existe.

session

Ce service représente les sessions. Voyez le fichier Symfony\Component\HttpFoundation\Session\Session.php pour en savoir plus. Sachez également que la session est accessible depuis la requête, en faisant $request->getSession(), donc si vous injectez déjà la requête dans votre service, inutile d'injecter également la session.

twig

Ce service représente une instance de Twig_Environment. Il permet d'afficher ou de retourner une vue. Vous pouvez en savoir plus en lisant la documentation de Twig. Ce service peut être utile pour modifier l'environnement de Twig depuis l’extérieur (lui ajouter des extensions, etc.).

templating

Ce service représente le moteur de templates de Symfony2. Par défaut il s'agit de Twig, mais cela peut également être PHP ou tout autre moteur intégré dans un bundle tiers. Ce service montre l'intérêt de l'injection de dépendances : en injectant templating et non twig dans votre service, vous faites un code valide pour plusieurs moteurs de templates ! Et si l'utilisateur de votre bundle utilise un moteur de templates à lui, votre bundle continuera de fonctionner. Sachez également que le raccourci du contrôleur $this->render() exécute en réalité $this->container->get('templating')->renderResponse().


En résumé

  • Les tags permettent de récupérer tous les services qui remplissent une même fonction : cela ouvre les possibilités pour les extensions Twig, les évènements, etc.
  • Les calls permettent les dépendances optionnelles, et facilitent l'intégration de bibliothèques tierces.
  • Les scopes sont un point particulier à connaître pour maîtriser complètement le conteneur de services.
  • Les principaux noms de services sont à connaître par cœur afin d'injecter ceux nécessaires dans les services que vous créerez !