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

Le DIC, ce chef d'orchestre!

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Le DIC, ce chef d'orchestre!

Avatar for Adrien Brault

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