Aller au contenu

Programmation PHP avec Symfony/Service

Un livre de Wikilivres.

Le principe des services Symfony est d'éviter d'instancier la plupart des classes avec des "new" dispersés dans le code, pour les déclarer une seule fois, grâce au container. Ils sont alors instanciés uniquement s'ils sont utilisés (ex : sur la page web courante), grâce au lazy loading du container[1].

Cette déclaration peut se faire en PHP, en YAML ou en XML. On baptise alors le service (il peut y en avoir plusieurs par classe), et on appelle ses arguments par leur nom de service. Exemple :

services:
    app.my_namespace.my_service:
        class: App\myNamespace\myServiceClass
        arguments:
            - '%parameter%'
            - '@app.my_namespace.my_other_service'

Pas de include ou require

[modifier | modifier le wikicode]

Les classes natives de PHP doivent être introduites par leur namespace ou bien par l'espace de nom global. Ex :

use DateTime;
echo new DateTime();

ou

echo new \DateTime();

Avant SF2.8, il était obligatoire de déclarer chaque service dans les fichiers de configuration .yml ou .yaml, en plus de leurs classes .php (qui peuvent se contredire), et de les mettre à jour à chaque changement de structure.

Depuis SF2.8, l'"autoconfigure: true" permet de déclarer automatiquement chaque service à partir de sa classe, et l'"autowiring: true" d'injecter automatiquement les arguments connus (ex : une autre classe appelée par son espace de nom et son nom), donc sans déclaration manuelle[2].

Depuis SF4, cette déclaration est par défaut sans le fichier services.yaml, mais on peut la placer dans un autre fichier qui sera importé par le premier, par exemple avec :

imports:
    - { resource: services1.yaml }
    - { resource: services2.yaml }

ou :

imports:
    - { resource: services/* }

Logo

Cette séparation des services en plusieurs .yaml nécessite par contre d'exclure les dossiers de ces services de l'autowiring, et de reprendre la section _defaults dans le nouveau .yaml.

Exemple d'exclusion récursive de plusieurs dossiers de même nom, avec ** :

    App\:
        resource: '../src/*'
        exclude:
            - '../src/UnDossier'
            - '../src/**/Entity' # Tous les sous-dossiers "Entity"

Par défaut, l'autowiring ne fonctionne pas avec les classes avec des tags, ou ayant autre chose que des services dans leurs constructeurs[3]. Néanmoins pour injecter des scalaires automatiquement, il suffit que ces derniers soit déclarés aussi. Ex :

services:
    _defaults:
        bind:
            $salt: 'ma_chaine_de_caractères'
            $variableSymfony: '%kernel.project_dir%'
            $variableDEnvironnement: '%env(resolve:APP_DEBUG)%'

Pour ajouter un tag ou injecter un service si on implémente une interface. Ex :

services:
    _instanceof:
        Psr\Log\LoggerAwareInterface:
            calls:
                  - [ 'setLogger', [ '@logger' ] ]

Ici, toutes les classes qui implémentent LoggerAwareInterface verront leurs méthodes setLogger(LoggerInterface $logger) appelées automatiquement à l’instanciation.

Les contrôleurs sont des services qui peuvent en appeler avec la méthode héritée de leur classe mère :

$this->get('app.my_namespace.my_service')

Pour déterminer si un service existe depuis un contrôleur :

$this->getContainer->hasDefinition('app.my_namespace.my_service')

Chaque service doit donc être déclaré avec un paramètre "class", puis peut ensuite facultativement contenir les paramètres suivants :

Paramètres des services en YAML
Nom Rôle
class Nom de la classe instanciée par le service.
arguments Tableau des arguments du constructeur de la classe, services ou variables.
calls Tableau des méthodes de la classe à lancer après l'instanciation, généralement des setters.
factory Instancie la classe depuis une autre classe donnée. Méthode statique de la classe qui sera renvoyée par le service[4].
configurator Exécute un invocable donné après l'instanciation de la classe[5].
alias Crée un autre nom pour un service, qui peut alors être modifié par d'autres paramètres de déclaration (ex : créer une version publique d'un service privé dans services_test.yaml[6]).
parent Nom de la superclasse.
abstract Booléen indiquant si la méthode est abstraite.
public Booléen indiquant une portée publique du service.
shared Booléen indiquant un singleton.
tags Quand on doit injecter un nombre indéterminé de services dans un autre, il est possible de le définir avec chacun des services à injecter, en y ajoutant un tag avec le nom du service qui peut les appeler. Ce tag doit néanmoins être défini dans un CompilerPass[7].
autowire Booléen vrai par défaut, spécifiant si le framework doit injecter automatiquement les arguments du constructeur.
decorates Remplace un service par sa version décorée (mais l'ancien est toujours accessible an ajoutant le suffixe .inner au service décorateur)[8]

Injecter des services tagués

[modifier | modifier le wikicode]

Dans un constructeur :

    App\Service\FactoriesHandler:
        arguments:
            - !tagged_iterator app.factory

Dans une autre méthode :

    App\Service\FactoriesHandler:
        calls:
            - [ 'setFactories', [!tagged_iterator app.factory] ]

Par défaut, l'itérateur contient des clés numériques, mais on peut les personnaliser[9]. Ex :

    App\Factory\FactoryOne:
        tags:
            - { name: 'app.factory', my_key: 'factory_one' }

    App\Service\FactoriesHandler:
        arguments:
            - !tagged_iterator { tag: 'app.factory', key: 'my_key' }

Service abstrait

[modifier | modifier le wikicode]

Un service abstrait est un système de factorisation des injections par l'intermédiaire d'une classe abstraite. Par exemple si on veut que tous les contrôleurs héritent du service logger (comme l'exemple _instanceof ci-dessus), plus la méthode setLogger() de leur classe abstraite, sans avoir à toucher à leurs constructeurs :

    App\Controller\:
        resource: '../src/Controller'
        parent: App\Controller\AbstractEntitiesController
        tags: ['controller.service_arguments']

    App\Controller\AbstractEntitiesController:
        abstract: true
        autoconfigure: false
        calls:
            - [ 'setLogger', [ '@logger' ] ]