Aller au menu - Aller au contenu

Icône Les services, théorie et création

Avatar
Mise à jour : 09/02/2012
Difficulté : Intermédiaire Intermédiaire Durée d'étude : 2 heures
23 763 visites depuis 7 jours, dont 318 sur ce chapitre classé 14/778
Vous avez souvent eu besoin d'exécuter une certaine fonction à plusieurs endroits différents dans votre code ? Alors ce chapitre et le prochain sont faits pour vous !

Nous allons découvrir ici une importante fonctionnalité de Symfony : le système de services. Vous le verrez, les services sont utilisés partout dans Symfony2, et sont une fonctionnalité incontournable pour commencer à développer sérieusement un site internet.

Afin de bien appréhender ce chapitre, même si rien n'est obligatoire, je vous conseille de lire ceci :

Ces deux tutoriels présentent l'injection de dépendance (l'injection de dépendance est liée au principe de service) de manière concise. Cependant, comme vous pourrez le voir, Symfony va bien plus loin ;) .

Commençons !
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Qu'est ce qu'un service ?

Un service est simplement un objet PHP qui rempli une fonction.

Cette fonction peut-être simple : envoyer des emails, 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). Il faut vraiment bien comprendre cela : un service est une simple classe.

Prenons pour exemple l'envoi d'e-mails. On pourrait alors créer une classe avec comme nom Mailer et la définir comme un service. Elle deviendrait alors utilisable n'importe où.

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

La persistance des services


Dans Symfony, chaque service est "persistant". Cela signifie simplement que la classe est instanciée une fois (à la première récupération du service) puis cette instance est la même par la suite. Cette persistance permet de manipuler très facilement les services tout au long du processus ;) .

Le service container


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 leurs associations avec un "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 de pouvoir utiliser une classe sans connaitre son nom. Par exemple : je cherche à envoyer un e-mail. Je demande au service container de me retourner le service nommé "Mailer", bien que je ne connaisse pas quelle classe est derrière ce nom. Le conteneur me renvoie une instance de la classe associée (SwiftMailer par exemple), que je peux utiliser pour envoyer mon e-mail. Cela permet de pouvoir changer de classe pour un service donné, sans pour autant changer le code de tous les endroits où le service est utilisé.

Voilà un schéma explicatif du positionnement du conteneur de services :

Fonctionnement du Service Container
Fonctionnement du Service Container


Dans Symfony, il existe aussi un ServiceContainer, mais celui-ci est différent de ceux présents dans les tutoriels que je vous ai recommandé. En effet, il associe non pas une méthode avec une classe mais un "identifiant". Vous comprendrez mieux ce que je veux dire dans la prochaine partie :) .

En pratique

Continuons sur notre exemple d'e-mail. Dans Symfony, il existe un composant appelé Swiftmailer, qui permet d'envoyer des e-mails simplement. Il est présent par défaut dans Symfony, sous forme de service.

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

Code : PHP
1
2
<?php
$container->get('mailer');


Pour avoir la liste des services disponibles, utilisez la commande Code : Console
php app/console container:debug


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


En effet, la question est importante. Dans Symfony il existe un classe nommée ContainerAware qui possède un attribut $container. Le cœur de Symfony alimente ainsi les classes du framework en utilisant la méthode setContainer(). Donc pour toute classe de Symfony héritant de ContainerAware, on peut faire ceci :

Code : PHP
1
2
<?php
$this->container->get('mailer');


Heureusement pour nous, la classe de base des contrôleurs nommée Controller hérite de cette classe ContainerAware, on peut appliquer ceci aux contrôleurs :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

namespace Sdz\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller; // Cette classe étend ContainerAware

class BlogController extends Controller
{
    public function indexAction()
    {
    	$swiftmailer = $this->container->get('mailer'); // On a donc accès au conteneur
    	
    	var_dump($swiftmailer);
    	exit;
    	
        return $this->render('SdzBlogBundle:Blog:index.html.twig');
    }
}


Il se peut que vous ayez déjà rencontré, depuis un contrôleur, l'utilisation de get() sans passer par l'attribut container, comme ceci : $this->get('mailer'). C'est parce que la classe Controller fournit un raccourci, la méthode $this->get() faisant simplement appel à la méthode $this->container->get().

Donc dans un contrôleur, $this->get('...') est strictement équivalent à $this->container->get('...').

Créer un service

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. Par exemple, la classe Sdz\BlogBundle\Service\SdzAntispam doit se trouver dans le répertoire src/Sdz/BlogBundle/Service/SdzAntispam.php. C'est ce que nous faisons depuis le début du tutoriel :) .

Je vous propose, pour suivre notre fil rouge du blog, 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 articles et pour les commentaires), faisons-en un service. Ce service devra être réutilisable simplement dans d'autre projets Symfony : il ne devra pas être dépendant d'un élément de notre blog. Je nommerais ce service SdzAntispam, mais vous pouvez le nommer comme vous le souhaitez. Il n'y a pas de règle précise à ce niveau, mise à part que l'utilisation des underscores (_) est fortement déconseillée.

Si votre service est relativement indépendant (il n'y a pas d'autres services autour du même thème), il est bon de mettre ce service dans un dossier Service de votre bundle, mais à vrai dire vous pouvez faire comme vous le souhaitez.

Créons donc le fichier src/Sdz/BlogBundle/Service/SdzAntispam.php, avec ce code pour l'instant :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
// src/Sdz/BlogBundle/Service/SdzAntispam.php

namespace Sdz\BlogBundle\Service;

/**
 * Un anti-spam simple pour Symfony2.
 *
 * @author Leglopin
 */
class SdzAntispam
{

}


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, faisons un rapide anti-spam : considérons qu'un message est un spam s'il contient au moins 3 liens ou adresse e-mail. Voici ce que j'obtiens :
Code : PHP
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
// src/Sdz/BlogBundle/Service/SdzAntispam.php

namespace Sdz\BlogBundle\Service;

/**
 * Un anti-spam simple pour Symfony2.
 *
 * @author Leglopin
 */
class SdzAntispam
{
    /**
     * Vérifie si le texte est un spam ou non.
     * Un texte est considéré comme spam à partir de 3 liens
     * ou adresses e-mails dans son contenu.
     * 
     * @param string $text
     */
    public function isSpam($text)
    {
        if( ($this->countLinks($text) + $this->countMails($text)) >= 3 )
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    /**
     * Compte les URL de $text.
     * 
     * @param string $text
     */
    private function countLinks($text)
    {
        preg_match_all(
            '#(http|https|ftp)://([A-Z0-9][A-Z0-9_-]*(?:.[A-Z0-9][A-Z0-9_-]*)+):?(d+)?/?#i',
            $text,
            $matches);
        
        return count($matches[0]);
    }
    
    /**
     * Compte les e-mails de $text.
     * 
     * @param string $text
     */
    private function countMails($text)
    {
        preg_match_all(
            '#[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}#i',
            $text,
            $matches);
        
        return count($matches[0]);
    }
}


Création de la configuration du service


Maintenant que nous avons créé notre service, il faut le signaler à Symfony. Un service se définit par sa classe ainsi que sa configuration. Pour cela, nous pouvons utiliser le fichier src/Sdz/BlogBundle/Ressources/config/services.yml.

Si vous avez généré votre bundle avec le generator en répondant "oui" pour créer toute la structure du bundle, alors ce fichier est chargé automatiquement. Vérifiez-le en confirmant que le répertoire DependencyInjection de votre bundle existe, il devrait contenir le fichier SdzBlogExtension.php.

Si ce n'est pas le cas, vous devez créer le fichier DependencyInjection/SdzBlogExtension.php (adaptez à votre bundle évidemment) avec le contenu suivant :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

namespace Sdz\BlogBundle\DependencyInjection;

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

class SdzBlogExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }
}


Ouvrez ou créez le fichier Ressources/config/services.yml de votre bundle, et ajoutez-y la configuration pour notre service :
Code : Autre
1
2
3
services:
    sdz_blog.antispam:
        class: Sdz\BlogBundle\Service\SdzAntispam


Dans cette configuration :
  • "sdz_blog.antispam" est le nom de notre service fraîchement créé. De cette manière, le service sera accessible via <?php $container->get('sdz_blog.antispam');. Essayez de respecter cette convention de préfixer le nom de vos services par "nomApplication_nomBundle". Pour notre bundle Sdz\BlogBundle, on a donc préfixé notre service de "sdz_blog.".
  • "class" est un attribut obligatoire de notre service, il définit simplement le namespace complet de la classe du service.

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

Sachez également que le conteneur de Symfony2 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 fichier services.yml :
Code : Autre
1
2
3
4
5
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 <?php $container->getParameter($identifiant); au lieu de get().


Utilisation du service


Maintenant que notre classe est définie, et notre configuration déclarée, nous pouvons nous servir du service. Voici un exemple simple de l'utilisation que l'on pourrait en faire :
Code : PHP
 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/Controller/BlogController.php

namespace Sdz\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function indexAction()
    {
        $antispam = $this->container->get('sdz_blog.antispam');

        if( $antispam->isSpam($text) )
        {
            exit('Votre message a été détecté comme spam !');
        }
    	
        return $this->render('SdzBlogBundle:Blog:index.html.twig');
    }
}


Et voilà, vous avez créé et utilisé votre premier service !
Les services sont extrêmement utilisés dans Symfony2. On n'a pas encore vu tout leur potentiel jusque maintenant, mais ils auront un aspect beaucoup plus intéressant au prochaine chapitre, où nous allons utiliser les services pour modifier le comportement de Symfony2 !
Chapitre précédent Sommaire Chapitre suivant

Partager

3 commentaires pour "Les services, théorie et création"
Note moyenne : 3.78 / 4 (191 votes)
Pseudo Commentaire
Hors ligne Ataris # Posté le 16/01/2012 à 18:33:59
Avatar

Avis : Très bon

Études : Ingésup Paris

Yeaahhh les revoilà ! Les fameux chapitres sur les services =D
J'attaque ça dès demain

Merci Winzou ! =)
Hors ligne ptosiani # Posté le 17/02/2012 à 12:54:01
Avatar

Avis : Très bon

Merci pour le tuto!

Un autre exemple pour utiliser le service antispam:

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public function voirAction($id)
    {
        $em = $this->getDoctrine()->getEntityManager();
        $article = $em->getRepository('SdzBlogBundle:Article')->find($id);
        $tags = $article->getTags();
        
        $antispam = $this->container->get('sdz_blog.antispam');
        if($antispam->isSpam($article->getContenu())){
            $article->setContenu('SPAM DETECTED, MODIFY YOUR POST');
        }
        
        $params = array(
            'article' => $article,
            'tags' => $tags
        );
        return $this->render('SdzBlogBundle:Blog:voir.html.twig',$params);
    }
Connecté winzou # Posté le 17/02/2012 à 13:02:05
lala
Avatar

Avis : Très bon Modérateurs

Ville : Singapour
Pays : Singapour
Études : Ecole Centrale de Lyon

C'est un peu dur quand même d'écraser tout ce que l'utilisateur a écrit par ton message :p Il faut utiliser la validation pour faire des beaux messages d'erreur tout en gardant le contenu de l'utilisateur ;)

Un tutoriel pour débuter avec le framework Symfony2.
Nouvelle partie entière sur Doctrine2 !
Étudiez pas à pas les entités, leur manipulation, les relations ainsi que les repositories :)

Je recherche quelqu'un capable de faire des icônes sympas pour les chapitres du tutoriel, contactez-moi, merci !
 

Voir tous les commentaires