A free and open-source book on ZF3 for beginners

Translation into this language is not yet finished. You can help this project by translating the chapters and contributing your changes.

3.10. Gestionnaire de services

Vous pouvez imaginer l'application comme un ensemble de services. Par exemple, vous pouvez avoir un service d'authentification responsable de la connexion des utilisateurs du site, d'un service de gestionnaire d'entités chargé d'accéder à la base de données, d'un service de gestion des événements chargé de déclencher les événements et de les transmettre aux écouteurs d'événements.

Dans Zend Framework, la classe ServiceManager est un conteneur centralisé pour tous les services d'application. Le gestionnaire de service est implémenté dans le composant Zend\ServiceManager en tant que classe ServiceManager. Le diagramme d'héritage de classe est montré dans la figure 3.5 ci-dessous :

Figure 3.5. Diagramme d'héritage de classes du gestionnaire de service Figure 3.5. Diagramme d'héritage de classes du gestionnaire de service

Le gestionnaire de service est créé au démarrage de l'application (à l'intérieur de la méthode statique init() de la classe Zend\Mvc\Application). Les services standard disponibles via le gestionnaire de services sont présentés dans le tableau 3.1. Cette table est incomplète car le nombre réel de services déclarés dans le gestionnaire de services peut être beaucoup plus important.

Table 3.1. Services standards
Nom du service Description
Application Permet de récupérer le singleton de la classe Zend\Mvc\Application.
ApplicationConfig Tableau de configuration extrait du fichier application.config.php.
Config Tableau de configuration extrait des fichiers module.config.php fusionné avec autoload/global.php et autoload/local.php.
EventManager Permet de récupérer une nouvelle instance de la classeZend\EventManager\EventManager. Le gestionnaire d'événements permet d'envoyer (déclencher) des événements et d'attacher des écouteurs d'événements.
SharedEventManager Permet de récupérer l'instance singleton de la classe Zend\EventManager\SharedEventManager.Le gestionnaire d'événements partagés permet d'écouter les événements définis par d'autres classes et composants.
ModuleManager Permet de récupérer le singleton de la classe Zend\ModuleManager\ModuleManager. Le gestionnaire de module est responsable du chargement des modules d'application.
Request Le singleton de la classe Zend\Http\Request. Représente la requête HTTP reçue du client.
Response Le singleton de la classe Zend\Http\Response. Représente la réponse HTTP qui sera envoyée au client.
Router Le singleton de Zend\Router\Http\TreeRouteStack. Effectue le routage d'URL.
ServiceManager Le gestionnaire de service lui même.
ViewManager Le singleton de la classe Zend\Mvc\View\Http\ViewManager. Responsable de la préparation de la vue pour le rendu de la page.

Un service est généralement composé d'une classe PHP basique, mais pas toujours. Par exemple, lorsque ZF3 charge les fichiers de configuration et fusionne les données dans des tableaux imbriqués, il déclare les tableaux dans le gestionnaire de services sous la forme d'un couple de services (!): ApplicationConfig et Config. Le premier est le tableau chargé à partir du fichier de configuration au niveau de l'application application.config.php et le second est le tableau fusionné des fichiers de configuration au niveau du module et des fichiers de configuration au niveau de l'application chargés automatiquement. Ainsi, dans le gestionnaire de services, vous pouvez stocker tout ce que vous voulez : une classe PHP, une variable simple ou un tableau.

À partir du tableau 3.1, vous pouvez voir que dans ZF3 presque tout peut être considéré comme un service. Le gestionnaire de services est lui-même déclaré en tant que tel. Ainsi que la classe Application.

Une chose importante que vous devez noter à propos des services est qu'ils sont stockés dans une seule instance (ceci est également appelé le modèle singleton). Évidemment, vous n'avez pas besoin de la deuxième instance de la classe Application (ce qui serait un vrai cauchemar).

Mais, il existe une exception importante à la règle ci-dessus. Ca peut être déroutant au début, mais l' EventManager n'est pas un singleton. Chaque fois que vous récupérez le service du gestionnaire d'événements auprès du gestionnaire de services, vous recevez un nouvel objet. Tout ça pour des raisons de performance et pour éviter d'éventuels conflits d'événements entre différents composants. Nous en discuterons plus en détail dans la section About Event Manager plus loin dans ce chapitre.

Le gestionnaire de services définit plusieurs méthodes nécessaires pour localiser et récupérer un service auprès du gestionnaire de services (voir le tableau 3.2 ci-dessous) :

Table 3.2. Méthodes du ServiceManager
Nom de la méthode Description
has($name) Vérifie si un tel service est déclaré.
get($name) Récupère l'instance d'un service déclaré.
build($name, $options) Renvoie toujours une nouvelle instance du service demandé..

Vous pouvez tester si un service est déclaré en transmettant son nom à la méthode has() du gestionnaire de services. Il renvoie le booléen vrai si le service est déclaré ou faux si un service avec un tel nom n'est pas déclaré.

Vous pouvez récupérer un service par son nom à l'aide de la méthode `get() du gestionnaire de services. Cette méthode prend un seul paramètre représentant le nom du service. Regardez l'exemple suivant :

<?php

// Récupère le tableau de configuration de l'application.
$appConfig = $serviceManager->get('ApplicationConfig');

// L'utilise (par exemple, en récupérant la liste des modules).
$modules = $appConfig['modules'];

Et la méthode build() qui crée toujours une nouvelle instance du service lorsque vous l'appelez (contrairement à get() qui crée l'instance du service une seule fois et la renvoie sur les requêtes ultérieures).

En règle générale, vous récupérez les services du gestionnaire de services non pas dans votre code mais à l'intérieur d'une fabrique. Une fabrique est un code responsable de la création d'un objet. Lors de la création de l'objet, vous pouvez extraire les services dont il dépend du gestionnaire de services et transmettre ces services (dépendances) au constructeur de l'objet. Ceci est également appelé injection de dépendance.

Si vous avez un peu d'expérience avec Zend Framework 2, vous remarquerez peut-être que les choses sont maintenant un peu différentes. Dans ZF2, il y avait un modèle ServiceLocator permettant d'obtenir des dépendances du gestionnaire de service dans n'importe quelle partie de votre application (dans les contrôleurs, les services, etc.). Dans ZF3, vous devez transmettre les dépendances explicitement. C'est un peu plus galère mais ça évite les dépendances "cachées" et rend votre code plus clair et plus facile à comprendre.

3.10.1. Déclarer un service

Lorsque vous codez votre site web, vous devrez souvent déclarer votre propre service dans le gestionnaire de services. L'un des moyens de déclarer un service consiste à utiliser la méthode setService() du gestionnaire de services. Par exemple, créons et déclarons la classe de service "convertisseur de devise", qui sera utilisée, par exemple, sur une page de panier pour convertir la devise EUR en USD:

<?php
// Définis l'espace de noms où notre service personnalisé est situé.
namespace Application\Service;

// Définis la classe de service CurrencyConverter.
class CurrencyConverter
{
    // Convertit les euros en dollars américains.
    public function convertEURtoUSD($amount)
    {
        return $amount*1.25;
    }

    //...
}

Ci-dessus, dans les lignes 6-15 nous définissons un exemple de classe CurrencyConverter (pour simplifier, nous implémentons une seule méthode convertEURtoUSD() qui est capable de convertir des euros en dollars américains).

// Création d'une instance de la classe.
$service = new CurrencyConverter();
// Déclaration de l'instance dans le gestionnaire de service.
$serviceManager->setService(CurrencyConverter::class, $service);

Dans l'exemple ci-dessus, nous instancions la classe avec le nouvel opérateur et l'enregistrons avec le gestionnaire de service en utilisant la méthode setService() (nous supposons que la variable $serviceManager est une classe de type Zend\ServiceManager\ServiceManager et qu'elle a été déclarée ailleurs).

La méthode setService() prend deux paramètres : le nom du service et l'instance du service. Le nom du service doit être unique dans tous les autres services possibles.

Une fois le service stocké dans le gestionnaire de services, vous pouvez le récupérer par son nom à n'importe quel endroit de votre application à l'aide de la méthode get() du gestionnaire de services. Regardez l'exemple suivant :

<?php
// Récupère le service de convertion de devises.
$service = $serviceManager->get(CurrencyConverter::class);

// Et l'utilise (convertis une somme d'argent).
$convertedAmount = $service->convertEURtoUSD(50);

3.10.2. Nommage des Services

Différents services peuvent utiliser différents styles de nommage. Par exemple, le même service de convertisseur de devises peut être chargé sous différents noms : CurrencyConverter, currency_converter et ainsi de suite. Pour introduire une convention de nommage uniforme, il est recommandé de charger un service par son nom de classe complet, comme cela :

$serviceManager->setService(CurrencyConverter::class);

Dans l'exemple ci-dessus, nous avons utilisé le mot-clé class. Il est disponible depuis PHP 5.5 et est utilisé pour la résolution de noms de classe. CurrencyConverter::class est appelé avec le nom complet de la classe comme \Application\Service\CurrencyConverter.

3.10.3. Remplacer un service existant

Si vous essayez de déclarer un nom de service qui est déjà présent, la méthode setService() lèvera une exception. Mais parfois vous voulez remplacer un service avec un service du même nom. Pour ce faire, vous pouvez utiliser la méthode setAllowOverride() du gestionnaire de services :

<?php
// Autorise le remplacement des services
$serviceManager->setAllowOverride(true);

// Sauvegarde l'instance dans le gestionnaire de service. Il n'y aura pas d'exception 
// même s'il y a un autre service avec le même nom.
$serviceManager->setService(CurrencyConverter::class, $service);

Ci-dessus, la méthode setAllowOverride() prend un paramètre booléen unique définissant si vous permettez de remplacer le service CurrencyConverter si un tel nom est déjà présent ou non.

3.10.4. Déclarer les classes invokable

Ce qui est dommage avec la méthode setService(), c'est que vous devez créer l'instance du service avant d'en avoir vraiment besoin. Si vous n'utilisez jamais le service, l'instanciation du service ne sera qu'une perte de temps et de mémoire. Pour résoudre ce problème, le gestionnaire de services vous fournit la méthode setInvokableClass().

<?php
// Déclare une classe invokable
$serviceManager->setInvokableClass(CurrencyConverter::class);

Dans l'exemple ci-dessus, nous transmettons au gestionnaire de service le nom de classe complet du service au lieu de transmettre son instance. Avec cette technique, le service sera instancié par le gestionnaire de service uniquement lorsque quelqu'un appelle la méthode get(CurrencyConverter::class). Ceci est également appelé lazy loading.

Les services dépendent souvent les uns des autres. Par exemple, le service de conversion de devises peut utiliser le service de gestionnaire d'entités pour lire les taux de change en base de données. L'inconvénient de la méthode setInvokableClass() est qu'elle ne permet pas de passer des paramètres (dépendances) au service lors de l'instanciation de l'objet. Pour résoudre ce problème, vous pouvez utiliser des fabriques, comme décrit ci-dessous.

3.10.5. Déclarer une fabrique

Une fabrique (factory en anglais) est une classe qui ne peut faire qu'une seule chose - créer d'autres objets.

Vous déclarez une fabrique pour un service avec la méthode setFactory() du gestionnaire de service :

La fabrique la plus simple est InvokableFactory - elle est analogue à la méthode setInvokableClass()` de la section précédente.

<?php
use Zend\ServiceManager\Factory\InvokableFactory;

// L'équivalent à la méthode setInvokableClass () de la section précédente.
$serviceManager->setFactory(CurrencyConverter::class, InvokableFactory::class);

Après avoir enregistré la fabrique, vous pouvez récupérer le service auprès du gestionnaire de service comme d'habitude avec la méthode get(). Le service sera instancié uniquement lorsque vous l'extrayez du gestionnaire de service (lazy loading).

Parfois, l'instanciation de service est plus complexe que la simple création de l'instance de service avec l'opérateur new (comme le fait InvokableFactory). Vous devrez peut-être passer certains paramètres au constructeur du service ou appeler certaines méthodes juste après sa construction. Cette logique d'instanciation complexe peut être encapsulée dans votre propre classe de fabrique personnalisée. La classe de fabrique implémente alors l'interface FactoryInterface :

<?php
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

interface FactoryInterface
{
    public function __invoke(ContainerInterface $container,
                        $requestedName, array $options = null);
}

Comme nous le voyons dans la définition de FactoryInterface, la classe factory doit fournir la méthode magique __invoke renvoyant l'instance d'un seul service. Le gestionnaire de service est transmis à la méthode __invoke en tant que paramètre $container; il peut être utilisé lors de la construction du service pour accéder à d'autres services (pour injecter des dépendances). Le deuxième argument ($requestedName) est le nom du service. Le troisième argument ($options) peut être utilisé pour transmettre certains paramètres au service et n'est utilisé que lorsque vous demandez le service avec la méthode build() du gestionnaire de services.

A titre d'exemple, écrivons une fabrique pour notre service de convertisseur de devises (voir le code ci-dessous). Nous n'utilisons pas de logiques de construction complexes pour notre service CurrencyConverter mais pour des services plus complexes, vous devrez peut-être en utiliser un. (???)

<?php
namespace Application\Service\Factory;

use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Service\CurrencyConverter;

// Factory class
class CurrencyConverterFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container,
                     $requestedName, array $options = null)
    {
        // Create an instance of the class.
        $service = new CurrencyConverter();

        return $service;
    }
}

Techniquement, dans ZF3 vous pouvez utiliser la même classe de fabrique pour instancier plusieurs services qui ont un code d'instanciation similaire (pour cela, vous pouvez utiliser l'argument $requestedName passé à la méthode __invoke() de la fabrique). Cependant, la plupart du temps, vous allez créer une fabrique différente pour chaque service.

3.10.6. Déclarer une fabrique abstraite

Un cas encore plus complexe d'usage de classe fabrique est lorsque vous devez déterminer au moment de l'exécution quels noms de service doivent être enregistrés. Pour une telle situation, vous pouvez utiliser une fabrique abstraite. Une classe de fabrique abstraite doit implémenter l'interface AbstractFactoryInterface :

<?php
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

interface AbstractFactoryInterface extends FactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName);
}

Une fabrique abstraite a deux méthodes : canCreate() et __invoke(). La première est nécessaire pour tester si la fabrique peut créer le service avec un certain nom et le second permet réellement de créer le service. Les méthodes prennent deux paramètres : le service manager ($container) et le nom du service ($requestedName).

La différence une classe de fabrique basique est qu'elle ne crée généralement qu'un seul type de service mais une fabrique abstraite peut créer dynamiquement autant de types de services qu'elle le souhaite.

Vous déclarez une fabrique abstraite avec la méthode setAbstractFactory() du gestionnaire de service.

Les fabriques abstraites sont une fonctionnalité puissante mais vous ne devriez les utiliser que lorsque c'est vraiment nécessaire car elles ont un impact négatif sur les performances. Il est préférable d'utiliser les fabriques habituelles (non abstraites).

3.10.7. Déclarer un Alias de Service

Parfois, vous pouvez définir un alias pour un service. L'alias est comme un lien symbolique : il fait référence à un service déjà déclaré. Pour créer un alias, utilisez la méthode setAlias() du gestionnaire de services :

<?php
// Déclare un alias pour le service CurrencyConverter
$serviceManager->setAlias('CurConv', CurrencyConverter::class);

Une fois déclaré, vous pouvez récupérer le service par son nom ou son alias à l'aide de la méthode get() du gestionnaire de services.

3.10.8. Services partagés et non partagés

Par défaut, les services sont stockés dans le gestionnaire de services dans une seule instance. Egalement appelé modèle de conception singleton. Par exemple, lorsque vous essayez de récupérer le service CurrencyConverter deux fois, vous recevrez le même objet. On appelle celà un service partagé.

Mais, dans certaines situations (rares), vous devrez créer une nouvelle instance d'un service à chaque fois que quelqu'un le demandera au gestionnaire de services. Un bon exemple est l'EventManager - vous obtenez une nouvelle instance à chaque fois que vous l'appelez.

Pour définir un service comme étant non partagé, vous pouvez utiliser la méthode setShared() du gestionnaire de service :

$serviceManager->setShared('EventManager', false);

3.10.9. Configuration du gestionnaire de service

Sur votre site internet, vous utilisez le fichier de configuration du gestionnaire de services pour déclarer vos services (au lieu d'appeler les méthodes du gestionnaire de service comme décrit ci-dessus).

Pour déclarer automatiquement un service dans le gestionnaire de service, la clé service_manager d'un fichier de configuration est généralement utilisée. Vous pouvez placer cette clé dans un fichier de configuration au niveau de l'application ou dans un fichier de configuration au niveau du module.

Si vous placez cette clé dans un fichier de configuration au niveau du module, faites attention au risque d'écrasement de nom lors de la fusion des configs. Ne déclarez pas le même nom de service dans différents modules.

Cette clé du service_manager devrait ressembler à ça :

<?php
return [
    //...

    // On déclare les services sous cette clé
    'service_manager' => [
        'services' => [
            // Ici les instances de classe service
            //...
        ],
        'invokables' => [
            // Ici les instances de classe service invokable
            //...
        ],
        'factories' => [
            // Ici les instances de classe fabrique
            //...
        ],
        'abstract_factories' => [
            // Ici les instances de classe abstraite fabrique
            //...
        ],
        'aliases' => [
            // Ici les alias des services déclarés au dessus
            //...
        ],
        'shared' => [
            // Spécifiez ici quels services ne doivent pas être partagés
        ]
  ],

  //...
];

Dans l'exemple ci-dessus, vous pouvez voir que la clé service_manager peut contenir plusieurs sous-clés pour déclarer des services de différentes manières :

À titre d'exemple, déclarons notre service CurrencyConverter et créons un alias pour celui-ci :

<?php
use Zend\ServiceManager\Factory\InvokableFactory;
use Application\Service\CurrencyConverter;

return [
    //...

    // On déclare les services sous cette clé
    'service_manager' => [
        'factories' => [
            // On déclare le service CurrencyConverter.
            CurrencyConverter::class => InvokableFactory::class
        ],
        'aliases' => [
            // On déclare un alias pour le service CurrencyConverter.
            'CurConv' => CurrencyConverter::class
        ],
  ],

  //...
];

Top