Introduction
Qu’est ce qu’un service ?
Un service est un objet qui permet d’altérer/créer de la donnée, il ne doit pas avoir d’état.
<?php
// the data
class RageFace
{
protected $name;
protected $image;
}
// The SERVICE
class RageFaceManager
{
protected $rageFaces = array();
public function getRageFace($name)
{
return new RageFace($name);
}
public function findRageFace($name)
{
// ...
}
}
Ne pas avoir d’état évite à un service de stocker de l’information qui pourrait générer un comportement aléatoire en fonction des appels ou encore d’agréger de l’information qui pourrait augmenter la consommation mémoire.
L’injection de dépendance
Il s’agit d’un design pattern où les besoins externes d’une classe ne sont pas initialisés dans la class, mais fournis lors de la création de l’objet.
<?php
// PAS BIEN
class RageFaceManager
{
protected $images;
public function __construct()
{
$this->images = Finder::create()->name('*.jpg')->in('image_folder');
}
}
// BIEN
class RageFaceManager
{
protected $images;
public function __construct(array $images)
{
$this->images = $images;
}
}
new RageFaceManager(Finder::create()->name('*.jpg')->in('image_folder'));
L’injection de dépendance permet de sortir les dépendances en dehors de la class et de limiter ses responsabilités. Les tests sont également plus faciles à réaliser.
Définition d’un service
La définition d’un service se fait via le fichier app/config/config.yml.
services:
rage_face.manager:
class: RageFace\Lib\RageFaceManager
arguments:
- ['image1.png', 'image2.png']
Résultat: appDevDebugProjectContainer.php
<?php
class appDevDebugProjectContainer extends Container
{
protected function getRageFace_ManagerService()
{
return $this->services['rage_face.manager'] = new \RageFace\Lib\RageFaceManager(array(
'image1.png',
'image2.png'
));
}
// + 20K lignes
}
Container d’injection de dépendances
Un container regroupe plusieurs services et permet de les récupérer via un identifiant unique dans le cas de Symfony2.
Le container de service peut être considéré comme tableau associatif service_id <=> service instance
. Il est possible de récupérer le service de la façon suivante:
<?php
$manager = $contaner->get('rage_face.manager');
Cycle de vie de la création du Container
La suite de la présentation va expliquer l’implémentation interne de la construction DIC dans SF2
RageFaceBundle
Bundle qui affiche des Troll Face provenant de RageFace :)
Migration de la définition
Création d’un bundle via la commande app/console generate:bundle, cette commande génére la structure suivante
╭─vagrant@debian ~/projects/sfpot-dic ‹master*›
╰─$ tree src/RageFace
src/RageFace
├── Lib
│ └── RageFaceManager.php
└── RageFaceBundle
├── DependencyInjection
│ ├── Configuration.php
│ └── RageFaceExtension.php
├── RageFaceBundle.php
└── Resources
└── config
└── services.xml
Migration de la configuration YAML vers du XML (format conseillé pour les bundles).
<services>
<service id="rage_face.manager" class="RageFace\Lib\RageFaceManager">
<argument type="collection">
<argument>image1.png</argument>
<argument>image2.png</argument>
</argument>
</service>
</services>
Le boostraping de Symfony2
Tout commence dans le front controller app.php
, qui initialise le container:
<?php
$kernel = new AppKernel('prod', false);
Le kernel est responsable de la création du container d’injection dépendances via certaines méthodes clés:
<?php
class Kernel {
// Initializes the service container.
protected function initializeContainer()
{}
// Returns the kernel parameters.
protected function getKernelParameters()
{}
// Builds the service container.
protected function buildContainer()
{}
// Gets a new ContainerBuilder instance used to build the service container.
protected function getContainerBuilder()
{}
// Dumps the service container to PHP code in the cache.
protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, $class, $baseClass)
{}
// Returns a loader for the container
protected function getContainerLoader(ContainerInterface $container)
{}
}
Le kernel utilise les informations provenant des bundles référencés dans la fonction AppKernel::registerBundles
, le bundle fournit cette information via la méthode BundleInterface::getContainerExtension
. La méthode doit retourner une instance de type ExtensionInterface
RageFaceExtension
<?php
class RageFaceExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
}
}
Configuration d’un bundle
Le code de l’extension montre deux éléments clés : l’objet Configuration
et l’objet XmlLoader
.
L’objet XmlLoader charge la définition des services et des paramètres présents dans le fichier cible.
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="rage_face.manager" class="RageFace\Lib\RageFaceManager">
<argument></argument>
</service>
</services>
</container>
rage_face:
images:
- 'image1.png'
- 'image2.png'
Cette configuration ne peut fonctionner sans définir les options dans l’objet Configuration. L’utilisation du fichier Configuration est optionelle, cependant en cas de non-utilisation, il faut gérer soit même les problématiques de merge.
<?php
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('rage_face');
$rootNode
->fixXmlConfig('extension')
->children()
->arrayNode('images')
->prototype('scalar')->end()
->end()
->end()
;
return $treeBuilder;
}
}
Voici la valeur de la variable $config
<?php
array(1) {
'images' => array(2) {
[0] => string(10) "image1.png"
[1] => string(10) "image2.png"
}
}
La dernière étape consiste à utiliser la valeur images
pour configurer le service via sa définition.
L’objet Definition
Cet objet contient toutes les informations nécessaires pour instancier un service:
- Les arguments à donner dans le constructeur: une valeur, une collection ou une Réference vers un service
- Les fonctions à appeler
- … et bien plus encore … scope, public/private et tags
Fichier définition:
<?php
class RageFaceExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
// alter the service definition to add an argument
$container->getDefinition('rage_face.manager')
->replaceArgument(0, $config['images']);
}
}
Des implémentations, un id : les alias
Il est possible d’avoir plusieurs implémentations pour un identifiant de service, cependant une seule instance est active à la suite de la création du container. Ce pattern est utilisé dans Symfony2 pour rajouter des loggers à certains services.
Voici un exemple avec le RageFaceManager, un nouveau service va être crée pour rajouter des logs lors d’appel à la méthode RageFaceManager::getFaces()
.
<services>
<!-- Le service a été renommé rage_face.manager => rage_face.manager.default -->
<service id="rage_face.manager.default" class="RageFace\Lib\RageFaceManager">
<argument></argument>
</service>
<service id="rage_face.manager.loggable" class="RageFace\Lib\RageFaceManagerLoggable">
<argument type="service" id="rage_face.manager.default" ></argument>
</service>
</services>
<?php
class RageFaceExtension extends Extension
{
/**
* {@inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
// alter the service definition to add an argument
$container->getDefinition('rage_face.manager.default')
->replaceArgument(0, $config['images']);
// create an alias
$debug = ($container->getParameter('kernel.debug');
$container->setAlias(
'rage_face.manager',
'rage_face.manager.'. $debug ? 'loggable' : 'default')
);
}
}
L’importance des interfaces
Dans l’exemple, nous avons 2 services identiques, cependant l’implémentation technique change. Il faut donc un dénominateur commun pour s’assurer que les services dépendant du service rage_face.manager fonctionneront toujours en fonction des implémentations.
RageFaceManagerInterface
<?php
interface RageFaceManagerInterface
{
public function getFace();
}
RageFaceManagerLoggable
<?php
class RageFaceManagerLoggable implements RageFaceManagerInterface
{
protected $manager;
protected $logger;
public function __construct(RageFaceManagerInterface $manager, LoggerInterface $logger = null)
{
$this->manager = $manager;
$this->logger = $logger;
}
public function getFace()
{
$face = $this->manager->getFace();
if ($this->logger) {
$this->logger->info(sprintf('Face %s => %s', $face->getName(), $face->getPath()));
}
return $face;
}
}
RageFaceManager
<?php
class RageFaceManager implements RageFaceManagerInterface
{
protected $images;
public function __construct(array $images)
{
$this->images = $images;
}
public function getFace()
{
$file = new \SplFileInfo(array_rand($this->images));
return new RageFace($file->getFilename(), $file->getPath());
}
}
ContainerBuilder
Les définitions de service sont stockées dans le ContainerBuilder; il existe un ContainerBuilder par bundle. Une extension ne peut pas modifier la définition d’un service présent dans un autre bundle.
Cependant il existe un ContainerBuilder principal, les définitions déclarées dans les bundles sont simplement mergées dans le container principal, cela se passe dans la class MergeExtensionConfigurationPass
.
La compilation
Le ContainerBuilder est un container de métadonnées, il ne peut pas être utilisé dans l’état, il doit être dumpé dans un fichier php => le fichier appDevDebugProjectContainer.php
.
Cependant avant de dumper le container, il doit être compilé:
- Pour vérifier l’intégrité d’un service
- Pour optimiser les appels
- Pour altérer la définition des services
Les étapes de la compilation
mergePass
: merge les ContainerBuildersbeforeOptimizationPasses
: contiens toutes les définitions de tous les servicesoptimizationPasses
: checks les servicesbeforeRemovingPasses
removingPasses
: supprime les services inutilesafterRemovingPasses
: le ContainerBuilder est maintenant prêt pour être mergé
Les constantes:
<?php
class PassConfig
{
const TYPE_AFTER_REMOVING = 'afterRemoving';
const TYPE_BEFORE_OPTIMIZATION = 'beforeOptimization';
const TYPE_BEFORE_REMOVING = 'beforeRemoving';
const TYPE_OPTIMIZE = 'optimization';
const TYPE_REMOVE = 'removing';
}
Les CompilerPass
Il en existe beaucoup dans Symfony2: AnalyzeServiceReferencesPass, CheckCircularReferencesPass, CheckDefinitionValidityPass, CheckExceptionOnInvalidReferenceBehaviorPass, CheckReferenceValidityPass, InlineServiceDefinitionsPass, MergeExtensionConfigurationPass, RemoveAbstractDefinitionsPass, RemovePrivateAliasesPass, RemoveUnusedDefinitionsPass, RepeatedPass, ReplaceAliasByActualDefinitionPass, ResolveDefinitionTemplatesPass, ResolveInvalidReferencesPass, ResolveParameterPlaceHoldersPass, ResolveReferencesToAliasesPass, RoutingResolverPass, ProfilerPass, RegisterKernelListenersPass, TemplatingPass, AddConstraintValidatorsPass, AddValidatorInitializersPass, FormPass, TranslatorPass, AddCacheWarmerPass, AddCacheClearerPass, TranslationExtractorPass, TranslationDumperPass, etc.
Exemple!
Rajoutons un RageFaceCompilerPass qui permet de définir des providers d’images.
Voici 3 nouvelles classes et donc 2 nouveaux services:
- ArrayProvider
- DirectoryProvider
- ProviderChain
Le fichier de définitions:
<service id="rage_face.provider.chain" class="RageFace\Lib\Provider\ProviderChain">
</service>
<service id="rage_face.provider.array" class="RageFace\Lib\Provider\ArrayProvider">
<argument ></argument>
<tag name="rage_face.provider"></tag>
</service>
Nous allons changer la signature du constructeur de la class RageFaceManager pour accepter un objet de type ProviderInterface:
<?php
class RageFaceManager implements RageFaceManagerInterface
{
protected $provider;
public function __construct(ProviderInterface $provider)
{
$this->provider = $provider;
}
public function getFace()
{
return array_rand($this->provider->getFiles());
}
}
Pour rester compatibles avec l’ancienne implémentation, nous allons juste modifier le nom du service qui accepte la liste des images dans le fichier RageFaceExtension. (enfin presque …)
<?php
$container->getDefinition('rage_face.provider.array')
->replaceArgument(0, $config['images']);
Maintenant il faut implémenter le CompilerPass.
<?php
class RageFaceCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$providerIds = $container->findTaggedServiceIds('rage_face.provider');
$chainDefinition = $container->getDefinition('rage_face.provider.chain');
foreach ($providerIds as $id => $tags) {
$chainDefinition->addMethodCall(
'addProvider',
array(new Reference($id)
));
}
}
}
<?php
class RageFaceBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new RageFaceCompilerPass());
}
}
Rajoutons un service dans le fichier config.yml
services:
my_rage_face.provider:
class: RageFace\Lib\Provider\DirectoryProvider
arguments:
- "%kernel.root_dir%/../web/rage-face"
tags:
- {name: rage_face.provider}
Résultat
<?php
protected function getRageFace_Provider_ChainService()
{
$this->services['rage_face.provider.chain'] = $instance = new \RageFace\Lib\Provider\ProviderChain();
$instance->addProvider($this->get('my_rage_face.provider'));
$instance->addProvider($this->get('rage_face.provider.array'));
return $instance;
}
Les CompilerPass: Private Service
Est ce que tous les services doivent être accessible dans le container ? Est ce qu’un service à besoin d’être exposé ? Si ce n’est pas cas => public=false
<service id="rage_face.provider.chain" class="RageFace\Lib\Provider\ProviderChain" public="false">
</service>
<service id="rage_face.provider.array" class="RageFace\Lib\Provider\ArrayProvider" public="false">
<argument ></argument>
<tag name="rage_face.provider"></tag>
</service>
Le résultat:
<?php
// rappel: le service sera accessible via un alias
protected function getRageFace_Manager_DefaultService()
{
$a = new \RageFace\Lib\Provider\ProviderChain();
$a->addProvider($this->get('my_rage_face.provider'));
$a->addProvider(new \RageFace\Lib\Provider\ArrayProvider(array(0 => 'image1.png', 1 => 'image2.png')));
return $this->services['rage_face.manager.default'] = new \RageFace\Lib\RageFaceManager($a);
}
Il faut maintenant le dumper, il existe plusieurs formats possibles: xml, php et graphviz
Encore là?
Retrouver son chemin
Il faut bien comprendre les étapes de création du container, deplus Symfony2 stocke des informations lors de la création du container. :
╭─vagrant@debian ~/projects/sfpot-dic ‹master*›
╰─$ head -n 5 app/cache/dev/appDevDebugProjectContainerCompiler.log
No directories configured for AnnotationConfigurationPass.
ResolveDefinitionTemplatesPass: Resolving inheritance for "templating.asset.default_package" (parent: templating.asset.path_package).
ResolveDefinitionTemplatesPass: Resolving inheritance for "security.user.provider.concrete.in_memory" (parent: security.user.provider.in_memory).
ResolveDefinitionTemplatesPass: Resolving inheritance for "security.user.provider.concrete.in_memory_user" (parent: security.user.provider.in_memory.user).
ResolveDefinitionTemplatesPass: Resolving inheritance for "security.user.provider.concrete.in_memory_admin" (parent: security.user.provider.in_memory.user).
ResolveDefinitionTemplatesPass: Resolving inheritance for "security.firewall.map.context.dev" (parent: security.firewall.context).
Il est possible de chercher un service via la commande app/console container:debug
╭─vagrant@debian ~/projects/sfpot-dic ‹master*›
╰─$ app/console container:debug | grep rage_face
my_rage_face.provider container RageFace\Lib\Provider\DirectoryProvider
rage_face.manager n/a alias for rage_face.manager.default
rage_face.manager.default container RageFace\Lib\RageFaceManager
rage_face.manager.loggable container RageFace\Lib\RageFaceManagerLoggable
Pour aller plus loin
- Scope
- Cache Metadata / ResourceInterface
- Class to compile
- Synchronized Service (Symfony 2.3)