Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Le DIC, ce chef d'orchestre!

Le DIC, ce chef d'orchestre!

Adrien Brault

April 09, 2015
Tweet

More Decks by Adrien Brault

Other Decks in Programming

Transcript

  1. About me • Adrien Brault • Software Engineer at Hautelook

    | Nordstrom Rack • Sensiolabs Certified Symfony Developer Expert • willdurand/Hateoas BazingaHateoasBundle • @AdrienBrault everywhere 2
  2. Timeline • Dependency Injection & Symfony Services • ContainerBuilder •

    Container Compilation • Patterns, Tips and Tricks 3
  3. class UserManager
 {
 private $db;
 
 public function __construct()
 {


    $this->db = new PDO(
 'mysql:host=localhost;dbname=testdb',
 'username',
 'password'
 );
 }
 }
 
 $manager = new UserManager(); % 6
  4. class UserManager
 {
 private $db;
 
 public function __construct(
 ContainerInterface

    $container
 ) {
 $this->db = $container->get('db');
 }
 }
 
 $container = new Container();
 $container->set(
 'db',
 new PDO(
 'mysql:host=localhost;dbname=testdb',
 'username',
 'password'
 )
 );
 $manager = new UserManager($container); % 7
  5. class UserManager
 {
 private $db;
 
 public function __construct(PDO $db)


    {
 $this->db = $db;
 }
 }
 
 $manager = new UserManager(
 new PDO(
 'mysql:host=localhost;dbname=testdb',
 'username',
 'password'
 )
 ); & 8
  6. Write services configuration <!-- src/AppBundle/Resources/config/services.xml -->
 
 <service id="user_manager"
 class="UserManager">


    <argument type="service" id="db"/>
 </service>
 
 <service id="db"
 class="PDO">
 <argument>mysql:host=localhost;dbname=testdb</argument>
 <argument>username</argument>
 <argument>password</argument>
 </service> 10
  7. Load the services config # app/config/config.yml
 imports:
 - { resource:

    "@AppBundle/Resources/config/services.xml" } OR // src/AppBundle/DependencyInjection/AppExtension.php
 class AppExtension extends Extension
 {
 public function load(
 array $configs,
 ContainerBuilder $container
 ) {
 $loader = new Loader\XmlFileLoader(
 $container,
 new FileLocator(
 __DIR__ . '/../Resources/config'
 )
 );
 $loader->load('services.xml');
 }
 } 11
  8. Remember this ? // AppBundle/DependencyInjection/AppExtension.php
 
 class AppExtension extends Extension


    {
 public function load(
 array $configs,
 ContainerBuilder $container
 ) {
 $loader = new Loader\XmlFileLoader(
 $container,
 new FileLocator(
 __DIR__ . '/../Resources/config'
 )
 );
 $loader->load('services.xml');
 }
 } 14
  9. XML/YAML maps to PHP <service id="db"
 class="PDO">
 <argument>mysql:host=localhost;dbname=testdb</argument>
 <argument>username</argument>
 <argument>password</argument>


    </service>
 use Symfony\Component\DependencyInjection\Definition; 
 $container->setDefinition(
 'db',
 new Definition('PDO', [
 'mysql:host=localhost;dbname=testdb',
 'username',
 'password',
 ])
 ); 㱺 15
  10. XML/YAML maps to PHP <service id="user_manager"
 class="UserManager">
 <argument type="service" id="db"/>


    </service> use Symfony\Component\DependencyInjection\Reference; 
 $container->setDefinition(
 'user_manager',
 new Definition('UserManager', [
 new Reference('db'),
 ])
 ); 㱺 16
  11. The ContainerBuilder and Definitions are mutable $container->removeDefinition('bad_bad_service'); 
 $container
 ->getDefinition('api_client')

    
 ->replaceArgument(1, new Reference('another_service')) 
 ->addMethodCall('setLogger', [new Reference('logger')]) 
 ->addTag('form.type', ['alias' => 'wow_such_form']) 
 ->setPublic(false)
 ; 17
  12. To learn more about the ContainerBuilder apis • Check out

    the PHP tab of the service container documentation examples • Symfony\Component\DependencyInjection\ • ContainerBuilder • Definition • Reference • Parameter • DefinitionDecorator • Alias • Scope 18
  13. ContainerBuilder is usable standalone $container = new ContainerBuilder();
 $container->setDefinition(...);
 $container->setDefinition(...);


    
 $db = $container->get('db');
 $manager = $container->get('user_manager');
 
 var_dump($db); // object(PDO)#8 (0) {}
 var_dump($manager); // object(UserManager)#9 (0) {} 19
  14. class appDevDebugProjectContainer extends Container
 {
 public function __construct()
 {
 //

    ...
 
 $this->methodMap = array(
 'form.resolved_type_factory' => 'getForm_ResolvedTypeFactoryService',
 );
 }
 
 protected function getForm_ResolvedTypeFactoryService()
 {
 return $this->services['form.resolved_type_factory'] =
 new ResolvedTypeFactoryDataCollectorProxy(
 new ResolvedFormTypeFactory(),
 $this->get('data_collector.form')
 )
 ;
 }
 
 // ...
 } 21
  15. Compilation Flow 1. Register bundles' DI extension 2. Let bundles

    add CompilerPasses 3. Read app/config/config.yml 4. Run CompilerPasses 4.1. First CompilerPass calls the DI extensions 4.2. All the other CompilerPasses run 23
  16. DI extensions interface ExtensionInterface
 {
 public function load( array $configs,

    ContainerBuilder $container );
 } • Sandboxed: the given ContainerBuilder is always empty (aside from the base parameters, like kernel.debug) • Can change its behaviour using the provided configuration 24
  17. CompilerPasses interface CompilerPassInterface
 {
 public function process(ContainerBuilder $container);
 } •

    Runs right before dumping the ContainerBuilder to PHP • Can change anything • A few Symfony ones optimise the ContainerBuilder • Most others allow interaction across bundles using tags 25
  18. Adding compiler passes 
 class AppBundle extends Bundle
 {
 public

    function build(ContainerBuilder $container)
 {
 $container->addCompilerPass(
 new DogeCompilerPass()
 );
 
 parent::build($container);
 }
 } 26
  19. class RestCompilerPass implements CompilerPassInterface
 {
 public function process(ContainerBuilder $container)
 {


    if (!$container->hasDefinition('jms_serializer')) {
 $container->removeDefinition(
 'rest.jms_event_subscriber'
 );
 }
 }
 } 27
  20. class BlackHoleCompilerPass implements CompilerPassInterface
 {
 public function process(ContainerBuilder $container)
 {


    foreach ($container->getServiceIds() as $id) {
 $container->removeDefinition($id);
 }
 }
 } 28
  21. Anonymous services <service id="user_manager"
 class="UserManager">
 <argument type="service">
 <service class="PDO">
 <argument>mysql:wow</argument>


    <argument>username</argument>
 <argument>password</argument>
 </service>
 </argument>
 </service> 31
  22. JSON/CSV injection at compilation 
 use Symfony\Component\Config\Resource\FileResource;
 
 class MemeExtension

    extends Extension
 {
 public function load(
 array $config,
 ContainerBuilder $container
 ) {
 // load services from XML/YAML
 
 $memesPath = __DIR__ . '/../Resources/config/memes.json';
 $memes = json_decode(file_get_contents($memesPath));
 
 $container
 ->getDefinition('meme_generator')
 ->replaceArgument(0, $memes)
 ;
 $container->addResource(new FileResource($memesPath));
 }
 } 32
  23. PrependExtension class RestExtension extends Extension
 implements PrependExtensionInterface
 {
 public function

    load(array $config, ContainerBuilder $container) { }
 
 public function prepend(ContainerBuilder $container)
 {
 $container->prependExtensionConfig(
 'framework',
 ['serializer' => 'true']
 );
 }
 } http://symfony.com/doc/current/cookbook/bundles/ prepend_extension.html 33
  24. Tags class NotificationSenderCompilerPass
 implements CompilerPassInterface
 {
 public function process(ContainerBuilder $container)


    {
 $chainDefinition = $container
 ->getDefinition('notification_chain');
 
 $services = $container
 ->findTaggedServiceIds('notification_sender');
 foreach ($services as $id => $tags) {
 foreach ($tags as $tagAttributes) {
 $chainDefinition->addMethodCall(
 'addSender',
 [new Reference($id)]
 );
 }
 }
 }
 } 34
  25. <service id="notification_chain"/> 
 <service id="email_sender">
 <tag name="notification_sender"/>
 </service> <service id="notification_sender">


    <call method="setMailer">
 <argument type="service" id="email_sender" />
 </call>
 </service>
 
 <service id="email_sender"/> http://symfony.com/doc/current/components/ dependency_injection/tags.html 35 㱺 after the compiler pass
  26. Use semantic configuration, not parameters (for reusable bundles ) class

    Configuration implements ConfigurationInterface
 {
 public function getConfigTreeBuilder()
 {
 $treeBuilder = new TreeBuilder();
 $rootNode = $treeBuilder->root('api');
 
 $rootNode
 ->children()
 ->arrayNode('rate_limit')
 ->canBeEnabled()
 ->children()
 ->integerNode('hourly_limit')->end()
 ->end()
 ->end()
 ->end()
 ;
 
 return $treeBuilder;
 }
 } 36
  27. class ApiExtension extends Extension
 {
 public function load(
 array $configs,


    ContainerBuilder $container
 ) {
 $config = $this->processConfiguration(
 new Configuration(),
 $configs
 );
 
 $loader = ...;
 
 if ($config['api']['rate_limit']['enabled']) {
 $loader->load('rate_limit.xml');
 }
 }
 } http://symfony.com/doc/current/cookbook/bundles/ configuration.html 37
  28. Automatic configuration processing use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
 
 class ApiExtension extends ConfigurableExtension


    {
 protected function loadInternal(
 array $config,
 ContainerBuilder $container
 ) {
 if ($config['api']['rate_limit']['enabled']) {
 // ...
 }
 }
 } http://symfony.com/doc/current/cookbook/bundles/ configuration.html 38