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
- Dépendances optionnelles : les calls
- Les champs d'application, ou scopes
- Les services courants de Symfony2
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 TwigcheckIfSpam()
. Ici, il s'agit de la méthodeisSpam()
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
:
- Vous appelez le service
Antispam
. Une instance de la classe est donc créée, appelons-laAntispam1
, avec comme argument une instance de la classeRequest
, appelons-laRequest1
. - 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 deRequest
, appelons-laRequest2
. - 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 à nouveauAntispam1
. Or, cette instanceAntispam1
contient l'objetRequest1
et nonRequest2
! 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 |
---|---|
|
Ce service est l'instance de l' |
|
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é. |
|
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 |
|
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 |
|
Ce service vous renvoie par défaut une instance de |
|
Ce service est très important : il vous donne une instance de |
|
Ce service vous donne accès au routeur ( |
|
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 |
|
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. |
|
Ce service représente les sessions. Voyez le fichier |
|
Ce service représente une instance de |
|
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 |
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 !