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

How I learned to Stop Wiring and Love Autowiring Containers

How I learned to Stop Wiring and Love Autowiring Containers

Does managing YAML, XML or PHP container configurations make you sad? Do you dread making changes to your classes' dependencies for fear of the inevitable container configuration wiring blues? Life doesn't have to be this way! Not if your container supports autowiring, that is. Hear one developer's journey into the wild world of containers, learn how autowiring works, and find out how using autowiring can free you from having to manually configure every dependency.

Beau Simensen

January 16, 2020
Tweet

More Decks by Beau Simensen

Other Decks in Programming

Transcript

  1. How I learned to Stop Wiring and Love Autowiring Containers

    Beau Simensen • beausimensen.com • @beausimensen
  2. Dependency Inversion .-- Dependency Injection --. | | Inversion of

    Control --+--------------------------+-- Container | | `-- Service Locator -------' Dependency Injection Container Inversion of Control Container Service Container
  3. Dependency Inversion .-- Dependency Injection --. | | Inversion of

    Control --+--------------------------+-- Container | | `-- Service Locator -------' Dependency Injection Container Inversion of Control Container Service Container Container
  4. Dependency Inversion .-- Dependency Injection --. | | Inversion of

    Control --+--------------------------+-- Container | | `-- Service Locator -------' Dependency Injection Container ## . Inversion of Control Container ## ## ## == Service Container ## ## ## ## === Container /""""""""""""""""\___/ === ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \______ o __/ (no, not that other type of container) \ \ __/ \____\______/
  5. services: app.word_list: class: 'AppBundle\Game\WordList' calls: - [ 'addWord', [ 'computer'

    ] ] - [ 'addWord', [ 'monitors' ] ] - [ 'addWord', [ 'cellular' ] ] public: false app.game_context: class: 'AppBundle\Game\GameContext' arguments: ['@session'] public: false app.game_runner: class: 'AppBundle\Game\GameRunner' arguments: ['@app.game_context', '@?app.word_list']
  6. public class RealBillingService implements BillingService { private final CreditCardProcessor processor;

    private final TransactionLog transactionLog; @Inject public RealBillingService(CreditCardProcessor processor, TransactionLog transactionLog) { this.processor = processor; this.transactionLog = transactionLog; } } Injector injector = Guice.createInjector(new BillingModule()); BillingService billingService = injector.getInstance( BillingService.class );
  7. That never happens but if it does there are ways.

    — Beau's entirely unconvincing friend
  8. $context->add('configuration', array( 'className' => 'dd_configuration_PropertiesConfiguration', 'constructorArgs' => array( 'locations' =>

    array( 'lithe_base.properties', 'app.properties', 'app.site.properties', ), ), )); $context->add('placeholderConfigurer', array( 'className' => 'substrate_DdConfigurationPlaceholderConfigurer', 'constructorArgs' => array( 'configuration' => $context->ref('configuration'), ), )); $context->add('logFactory', array( 'className' => 'dd_logging_LogFactory', ));
  9. foreach ($constructor->getParameters() as $reflectionParamter) { $constructorArgumentName = $reflectionParamter->getName(); $paramClass =

    $reflectionParamter->getClass(); if ($paramClass) { $paramClassName = $paramClass->getName(); foreach ($this->stoneInstances as $testStone) { if ($testStone instanceof $paramClassName) { $constructorArgs[] = $testStone; break; } } } }
  10. class dd_logging_FileLogDriver { public function __construct($filename) { /* .. */

    } } class dd_logging_LogFactory { public function __construct(dd_logging_FileLogDriver $logger) { /* .. */ } } $context->add('logger', array( 'className' => 'dd_logging_FileLogDriver', 'constructorArgs' => array( 'filename' => __DIR__.'/app.log', ), )); $context->add('logFactory', array( 'className' => 'dd_logging_LogFactory', ));
  11. class ImportantService { private $logger; public function __construct($logger) { $this->logger

    = $logger; } public function doImportantTask() { $this ->logger ->info('Did important task!'); } }
  12. # k.php class ImportantService { /* ... */ } $fileLogDriver

    = new FileLogDriver(__DIR__.'/app.log'); $logger = new Logger($fileLogDriver); $importantService = new ImportantService($logger); $importantService->doImportantTask();
  13. # services.yml services: fileLogDriver: class: 'FileLogDriver' arguments: ['%kernel.root_dir%/app.log'] logger: class:

    'Logger' arguments: ['@fileLogDriver'] importantService: class: 'ImportantService' arguments: ['@logger'] $importantService = $container->get('importantService'); $importantService->doImportantTask();
  14. <!-- app/config/services.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container> <services> <service

    id="fileLogDriver" class="FileLogDriver"> <argument>%kernel.root_dir%/app.log</argument> </service> <service id="logger" class="Logger"> <argument type="service" id="fileLogDriver" /> </service> <service id="importantService" class="ImportantService"> <argument type="service" id="logger" /> </service> </services> </container> $importantService = $container->get('importantService'); $importantService->doImportantTask();
  15. $container = new Pimple\Container(); $container['fileLogDriver'] = function () { return

    new FileLogDriver(__DIR__.'/app.log'); }; $container['logger'] = function ($c) { return new Logger($c['fileLogDriver']); }; $container['importantService'] = function ($c) { return new ImportantService($c['logger']); }; $importantService = $container['importantService']; $importantService->doImportantTask();
  16. $container = new Illuminate\Container\Container(); $container->singleton('fileLogDriver', function ($c) { return new

    FileLogDriver(__DIR__.'/app.log'); }); $container->singleton('logger', function ($c) { return new Logger($c->make('fileLogDriver')); }); $container->singleton('importantService', function ($c) { return new ImportantService($c->make('logger')); }); $importantService = $container->make('importantService'); $importantService->doImportantTask();
  17. $container = new Illuminate\Container\Container(); $container->singleton(FileLogDriver::class, function ($c) { return new

    FileLogDriver(__DIR__.'/app.log'); }); $container->singleton(Logger::class, function ($c) { return new Logger($c->make(FileLogDriver::class)); }); $container->singleton(ImportantService::class, function ($c) { return new ImportantService($c->make(Logger::class)); }); $importantService = $container->make(ImportantService::class); $importantService->doImportantTask();
  18. foreach ($constructor->getParameters() as $reflectionParamter) { $constructorArgumentName = $reflectionParamter->getName(); $paramClass =

    $reflectionParamter->getClass(); if ($paramClass) { $paramClassName = $paramClass->getName(); foreach ($this->stoneInstances as $testStone) { if ($testStone instanceof $paramClassName) { $constructorArgs[] = $testStone; break; } } } }
  19. foreach ($constructor->getParameters() as $reflectionParamter) { $constructorArgumentName = $reflectionParamter->getName(); $paramClass =

    $reflectionParamter->getClass(); if ($paramClass) { $paramClassName = $paramClass->getName(); $found = false; foreach ($this->stoneInstances as $testStone) { if ($testStone instanceof $paramClassName) { $constructorArgs[] = $testStone; $found = true; break; } } if (! $found) { $constructorArgs[] = $this->make($paramClassName); } } }
  20. $container = new Illuminate\Container\Container(); $container->singleton(FileLogDriver::class, function ($c) { return new

    FileLogDriver(__DIR__.'/app.log'); }); $container->singleton(Logger::class, function ($c) { return new Logger($c->make(FileLogDriver::class)); }); $container->singleton(ImportantService::class, function ($c) { return new ImportantService($c->make(Logger::class)); }); $importantService = $container->make(ImportantService::class); $importantService->doImportantTask();
  21. $container = new Illuminate\Container\Container(); $container->singleton(FileLogDriver::class, function ($c) { return new

    FileLogDriver(__DIR__.'/app.log'); }); $container->singleton(Logger::class, function ($c) { return new Logger($c->make(FileLogDriver::class)); }); $importantService = $container->make(ImportantService::class); $importantService->doImportantTask();
  22. $container = new Illuminate\Container\Container(); $container->singleton(FileLogDriver::class, function ($c) { return new

    FileLogDriver(__DIR__.'/app.log'); }); $importantService = $container->make(ImportantService::class); $importantService->doImportantTask();
  23. class ImportantService { private $logger; public function __construct($logger) { $this->logger

    = $logger; } public function doImportantTask() { $this ->logger ->info('Did important task!'); } }
  24. class ImportantService { private $logger; private $connection; public function __construct($logger,

    $connection) { $this->logger = $logger; $this->connection = $connection; } public function doImportantTask() { $this->connection->execute(); $this ->logger ->info('Did important task!'); } }
  25. # k.php class Connection { /* ... */ } class

    ImportantService { /* ... */ } $fileLogDriver = new FileLogDriver(__DIR__.'/app.log'); $logger = new Logger($fileLogDriver); $connection = new Connection(); $importantService = new ImportantService( $logger, $connection ); $importantService->doImportantTask();
  26. # services.yml services: fileLogDriver: class: 'FileLogDriver' arguments: ['%kernel.root_dir%/app.log'] logger: class:

    'Logger' arguments: ['@fileLogDriver'] importantService: class: 'ImportantService' arguments: ['@logger'] $importantService = $container->get('importantService'); $importantService->doImportantTask();
  27. # services.yml services: fileLogDriver: class: 'FileLogDriver' arguments: ['%kernel.root_dir%/app.log'] logger: class:

    'Logger' arguments: ['@fileLogDriver'] connection: class: 'Connection' importantService: class: 'ImportantService' arguments: ['@logger', '@connection'] $importantService = $container->get('importantService'); $importantService->doImportantTask();
  28. <!-- app/config/services.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container> <services> <service

    id="fileLogDriver" class="FileLogDriver"> <argument>%kernel.root_dir%/app.log</argument> </service> <service id="logger" class="Logger"> <argument type="service" id="fileLogDriver" /> </service> <service id="importantService" class="ImportantService"> <argument type="service" id="logger" /> </service> </services> </container> $importantService = $container->get('importantService'); $importantService->doImportantTask();
  29. <!-- app/config/services.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container> <services> <service

    id="fileLogDriver" class="FileLogDriver"> <argument>%kernel.root_dir%/app.log</argument> </service> <service id="logger" class="Logger"> <argument type="service" id="fileLogDriver" /> </service> <service id="connection" class="Connection" /> <service id="importantService" class="ImportantService"> <argument type="service" id="logger" /> <argument type="service" id="connection" /> </service> </services> </container> $importantService = $container->get('importantService'); $importantService->doImportantTask();
  30. $container = new Pimple\Container(); $container['fileLogDriver'] = function () { return

    new FileLogDriver(__DIR__.'/app.log'); }; $container['logger'] = function ($c) { return new Logger($c['fileLogDriver']); }; $container['importantService'] = function ($c) { return new ImportantService($c['logger']); }; $importantService = $container['importantService']; $importantService->doImportantTask();
  31. $container = new Pimple\Container(); $container['fileLogDriver'] = function () { return

    new FileLogDriver(__DIR__.'/app.log'); }; $container['logger'] = function ($c) { return new Logger($c['fileLogDriver']); }; $container['connection'] = function ($c) { return new Connection(); }; $container['importantService'] = function ($c) { return new ImportantService( $c['logger'], $c['connection'] ); }; $importantService = $container['importantService']; $importantService->doImportantTask();
  32. class Homepage { private $router; private $twig; public function __construct(RouterInterface

    $router, \Twig_Environment $twig) { $this->router = $router; $this->twig = $twig; } /** * @Route("/myaction", name="my_action") */ public function __invoke(Request $request) { if (!$request->isMethod('GET')) { return new RedirectResponse($this->router->generateUrl('my_action'), 301); } return new Response($this->twig->render('mytemplate.html.twig')); } }
  33. # before services: Foo: public: false autowire: true Bar: public:

    false autowire: true # after services: _defaults: public: false autowire: true Foo: ~ Bar: ~
  34. services: _defaults: public: false _instanceof: Symfony\Component\Console\Command\Command: tags: ['console.command'] public: true

    # commands must be public Twig_ExtensionInterface: tags: ['twig.extension'] Symfony\Component\EventDispatcher\EventSubscriberInterface: tags: ['kernel.event_subscriber'] App\: resource: ../src/{Action,Command,EventSubscriber,Twig}
  35. # before services: _defaults: autowire: true _instanceof: Symfony\Component\EventDispatcher\EventSubscriberInterface: tags: [kernel.event_subscriber]

    # service using the above tag AppBundle\EventListener\CheckRequirementsSubscriber: ~ # after services: _defaults: autowire: true autoconfigure: true # service using the auto_configure_instanceof functionality AppBundle\EventListener\CheckRequirementsSubscriber: ~
  36. class NewsletterManager { private $logger; private $em; private $apiKey; public

    function __construct( LoggerInterface $logger, EntityManager $em, $apiKey ) { $this->logger = $logger; $this->em = $em; $this->apiKey = $apiKey; } }
  37. # New awesome syntax services: _defaults: { autowire: true }

    Acme\NewsletterManager: { $apiKey: "%mandrill_api_key%" } # Alternative (more traditional) syntax services: newsletter_manager: class: Acme\NewsletterManager arguments: $apiKey: "%mandrill_api_key%" autowire: true
  38. class Home { /** * @Route("/myaction", name="my_action") */ public function

    myAction(Request $request, Router $router, Twig $twig) { if (!$request->isMethod('GET')) { return new RedirectResponse($router->generateUrl('my_action'), 301); } return new Response($twig->render('mytemplate.html.twig')); } }
  39. services: _defaults: autowire: true autoconfigure: true public: false App\: resource:

    '../../src/{Command,Form,EventSubscriber,Twig,Voter}' App\Controller\: resource: '../../src/Controller' public: true tags: ['controller.service_arguments']
  40. src/Controller/ ├── AccountBusinessFirstTimeSetupController.php ├── Businesses │ ├── Audiences │ │

    └── Connect │ │ └── ConnectTwitterAudienceController.php │ ├── AudiencesController.php │ ├── Stores │ │ └── Connect │ │ └── ConnectEtsyStoreController.php │ └── StoresController.php ├── DashboardController.php ├── PricingController.php ├── SecurityController.php └── UserController.php
  41. class SecurityController extends Controller { /** * @Route("/login", name="login", methods={"GET"})

    */ public function login(AuthenticationUtils $authUtils) { return $this->render('security/login.html.twig', [ 'last_username' => $authUtils->getLastUsername(), 'error' => $authUtils->getLastAuthenticationError(), ]); } }
  42. class ConnectEtsyStoreController extends Controller { private $serverFactory; private $oauthRepository; public

    function __construct( EtsyOauthServerFactory $serverFactory, EtsyOauthRepository $oauthRepository, ) { $this->serverFactory = $serverFactory; $this->oauthRepository = $oauthRepository; } /** * @Route("/account/businesses/{business}/stores/connect/etsy", name="etsy_store_connect") */ public function connect(Request $request, SessionInterface $session, Business $business) { $server = $this->serverFactory->create($business); $temporaryCredentials = $server->getTemporaryCredentials(); $session->set('etsyoauth_token', $temporaryCredentials->getIdentifier()); $session->set('etsyoauth_token_secret', $temporaryCredentials->getSecret()); return $this->redirect($server->getAuthorizationUrl($temporaryCredentials)); } }
  43. src/Integration/Store/Etsy/ ├── EtsyApiClient │ ├── EtsyApiClient.php │ ├── Requests │

    │ ├── Associations.php │ │ ├── Listing │ │ │ ├── FindAllShopListingsActive.php │ │ │ └── FindAllShopSectionListingsActive.php │ │ ├── Pagination.php │ │ ├── RenderedRequest.php │ │ ├── Request.php │ │ └── Shop │ │ └── FindAllShopSections.php │ └── Response.php └── Oauth ├── EtsyOauthServer.php └── EtsyOauthServerFactory.php
  44. class UniqueBusinessUsernameValidator extends ConstraintValidator { /** * @var BusinessRepository */

    private $businessRepository; public function __construct(BusinessRepository $businessRepository) { $this->businessRepository = $businessRepository; } public function validate($value, Constraint $constraint) { $business = $this->businessRepository->findByUsername($value); if (is_null($business)) { return; } $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $value) ->addViolation(); } }
  45. services: _defaults: autowire: true autoconfigure: true App\: resource: '../src/*' exclude:

    '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' App\Controller\: resource: '../src/Controller' tags: ['controller.service_arguments']
  46. # ... App\Integration\Store\Etsy\EtsyApiClient\EtsyApiClient: arguments: $consumerKey: '%env(ETSYOAUTH_CONSUMER_KEY)%' $consumerSecret: '%env(ETSYOAUTH_CONSUMER_SECRET)%' App\Integration\Store\Etsy\Oauth\EtsyOauthServerFactory: arguments:

    $consumerKey: '%env(ETSYOAUTH_CONSUMER_KEY)%' $consumerSecret: '%env(ETSYOAUTH_CONSUMER_SECRET)%' App\Controller\Businesses\Audiences\Connect\ConnectTwitterAudienceController: arguments: $consumerKey: '%env(TWITTEROAUTH_CONSUMER_KEY)%' $consumerSecret: '%env(TWITTEROAUTH_CONSUMER_SECRET)%' App\Doctrine\SchemaNamespaceFixListener: tags: - { name: doctrine.event_subscriber, connection: default }
  47. class Logger { public function __construct(FileLogDriver $fileLogDriver) { /* ...

    */ } } class FileLogDriver { public function info($message) { /* ... */ } }
  48. class Logger { public function __construct(Logger $logger) { /* ...

    */ } } interface LogDriver { public function info($message); } class FileLogDriver implements LogDriver { public function info($message) { /* ... */ } }
  49. # k.php class Connection { /* ... */ } class

    ImportantService { /* ... */ } class UnimportantService { /* ... */ } $fileLogDriver = new FileLogDriver(__DIR__.'/app.log'); $fileLogDriver = new Logger($fileLogDriver); $connection = new Connection(); $importantService = new ImportantService( $fileLogDriver, $connection ); $nullLogDriver = new NullLogDriver(); $nullLogger = new Logger($nullLogDriver); $unimportantService = new UnimportantService( $nullLogger );
  50. $container = new Illuminate\Container\Container(); $container ->when(FileLogDriver::class) ->needs('$filename') ->give(__DIR__.'/app.log'); $container->bind(LogDriver::class, FileLogDriver::class);

    $importantService = $container->make(ImportantService::class); $unimportantService = $container->make(UnimportantService::class);
  51. $container = new Illuminate\Container\Container(); $container ->when(FileLogDriver::class) ->needs('$filename') ->give(__DIR__.'/app.log'); $container->bind(LogDriver::class, FileLogDriver::class);

    $container ->when(UnimportantService::class) ->needs(LogDriver::class) ->give(function ($c) { $nullLogDriver = $c->make(NullLogDriver::class); return new Logger($nullLogDriver); }); $importantService = $container->make(ImportantService::class); $unimportantService = $container->make(UnimportantService::class);