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

DIC To The Limit – deSymfonyDay, Barcelona 2014

DIC To The Limit – deSymfonyDay, Barcelona 2014

Applying Dependency Inversion and Dependency Injections principles correctly in Symfony

Ronny López

May 31, 2014
Tweet

More Decks by Ronny López

Other Decks in Technology

Transcript

  1. Ronny López ABOUT ME US Hard way learner ! Technical

    Lead at Social Point Do stuff at TangoTree in the nights ! @ronnylt
 www.tangotree.io
 https://github.com/ronnylt
  2. AGENDA • The problems • Dependency Injection • Coupling done

    right • Dependency Inversion Principle • Types of DI • Dependency Injection Containers • Symfony Dependency Injection Container • Interchangeable Services
  3. THE PROBLEMS • We require to switch out data access

    layer without compromising all the other parts of the application • We need to use different implementations in different deployments (dragon game, monster game, etc…) • We wish to deploy the application in different environments (testing, integration, staging, production, etc…) • We need different configurations in each environment
  4. IN CASE YOU NEED IT • You have to do

    Dependency Injection, correctly • You have to know and apply Dependency Inversion Principles
  5. WHAT WE REALLY WANT IS • Code that is easy

    to test • Clear separation of infrastructure logic from application logic • Interchangeable infrastructure
  6. WHY? • Multiple deployments share parts of the same code,

    but each deployment has specific infrastructure needs • Multiple environments with different needs • We want to automatically test all the things • We want to go fast, and the only way to go fast is… you know…
  7. COUPLING Mod A Mod B Mod C Mod D Tight

    (high, strong) Mod A Mod B Mod C Mod D Loose (low, weak) Mod A Mod B Mod C Mod D None Coupling is a measure of the independency of components/modules
  8. COUPLING • The history of software shows that coupling is

    bad, but it also suggest that coupling is unavoidable • An absolutely decoupled application is useless because it adds no value • Developers can only add value by coupling things together
  9. COUPLING DONE RIGHT • Components make no assumptions about what

    other components do, but rely on their contracts • Use an interface to define a type and focus on what is important • Concrete implementations of the interfaces should be be instantiated outside the component
  10. DEPENDENCY INVERSION • Decouple high level parts of the system

    from low level parts by using interfaces
  11. TYPES OF DI • Constructor Injection • Setter Injection •

    Interface Injection • Property Injection • Service Locator
  12. CONSTRUCTOR INJECTION • Injects the dependencies via constructor • It

    ensures that the choice of dependency is immutable ! class EnergyBuyCommandHandler implements CommandHandler! {! private $playerRepository;! ! private $configRepository;! ! public function __construct(! ! ! PlayerRepository $playerRepository, ! ! ! ConfigRepository $config! ! )! {! $this->playerRepository = $playerRepository;! $this->configRepository = $config;! }! }!
  13. CONSTRUCTOR INJECTION • It ensures that the choice of dependency

    is immutable • The constructor is only ever called once when the object is created, so you can be sure that the dependency will not change during the object's lifetime Pros Cons • It is not suitable for working with optional dependencies • Difficult to use in combination with class hierarchies
  14. CONSTRUCTOR INJECTION • Try to always use constructor injection •

    If dealing with legacy code that does not support it, consider using an adapter class with constructor injection • Depends on abstractions (interfaces), not concrete implementations TIPS
  15. SETTER INJECTION • Inject the dependencies via a setter method

    • The “injector” has to call the method in order to inject the dependency ! class LoggerChain implements SQLLogger! {! private $loggers = array();! ! public function setLogger(SQLLogger $logger)! {! $this->logger = $logger;! }! }!
  16. SETTER INJECTION • Works “well” with optional dependencies
 
 If

    you do not need the dependency, then just do not call the setter • You can call the setter multiple times.
 
 This is particularly useful if the method adds the dependency to a collection Pros Cons • Works “well” with optional dependencies 
 
 Are you sure you need optional dependencies? • You can call the setter multiple times • You are not sure if the dependency was set
  17. SETTER INJECTION TIPS • Avoid setter injections (the choice of

    dependencies is not inmutable) • If you do Dependency Inversion right, probably YANGI • Remember, your classes depend on abstractions, not concrete implementations, so you can use Null or Dummy implementations when necessary
  18. SETTER INJECTION EXAMPLE use Psr\Log\LoggerInterface;! ! final class OrderProcessor! {!

    private $logger;! ! function __construct(. . ., LoggerInterface $logger)! {! $this->logger = $logger;! }! }! ! final class GoodManLogger implements LoggerInterface {…}! ! final class LogstarLogger implements LoggerInterface {…}! ! final class NullLogger implements LoggerInterface {…}! Instead of having a setter method to inject the logger, use constructor injection and use the appropriate logger implementation in each case
  19. INTERFACE INJECTION • Define and use interfaces for the injection

    • Allows certain objects to be injected into other objects, that implement a common interface • It’s a kind of setter injection, so same pros and cons interface ContainerAwareInterface! {! public function setContainer(ContainerInterface $container = null);! }! ! class ContainerAwareEventDispatcher implements ContainerAwareInterface! {! public function setContainer(ContainerInterface $container = null)! {! }! }!
  20. PROPERTY INJECTION • Allows setting public fields of the class

    directly • There are mainly only disadvantages to using property injection, it is similar to setter injection but with additional important problems ! ! class NewsletterManager! {! public $mailer;! ! // ...! }!
  21. PROPERTY INJECTION • Useful if you are working with code

    that is out of your control, such as in a 3rd party library, which uses public properties for its dependencies Pros Cons • You cannot control when the dependency is set at all, it can be changed at any point in the object's lifetime • You cannot use type hinting so you cannot be sure what dependency is injected except by writing into the class code to explicitly test the class instance before using it
  22. SERVICES LOCATOR • Is an object that knows how to

    get all of the services that an another service might need interface CommandHandlerLocator ! {! public function locate($commandName);! }! ! ! class ContainerCommandHandlerLocator implements CommandHandlerLocator! {! private $container;! ! public function __construct(ContainerInterface $container)! {! $this->container = $container;! }! ! public function locate($commandName)! {! if (!$this->container->has($commandName)) {! throw new NotFoundException('Unable to find command handler');! }! ! return $this->container->get($commandName);! }! }!
  23. SERVICE LOCATOR • It’s easy to use and abuse due

    to its straightforward behaviour • Not all use cases are bad, for example when you want to load services on demand at runtime Pros Cons • It hides dependencies in your code making them difficult to figure out and potentially leads to errors that only manifest themselves at runtime • It becomes unclear what are the dependencies of a given class • It’s easy to abuse • It’s consider and anti-pattern (but there are valid use cases for it)
  24. SERVICE LOCATOR TIPS • Use a segregated interface for the

    locator, do not depends on the whole locator (container) ! • Limit the types of services a locator provides
  25. DIC • Most of the time you do not need

    a DIC to benefit from Dependency Injection • But… creating and maintaining the dependencies by hand can become a nightmare pretty fast • A DIC manages objects from their instantiation to their configuration • The objects themselves should not know that they are managed by a container and should not know nothing about it
  26. PHP DIC IMPLEMENTATIONS • Twittee • Pimple • Illuminate\Di •

    Zend\Di • Symfony\DependencyInjection • http://php-di.org/ • Build your own
  27. The Dependency Injection component allows you to standardize and centralize

    the way objects are constructed in a SF application
  28. HOW IT WORKS? • Reads definition of how objects (services)

    should be constructed (XML, YAML, PHP, etc…) • Collects all definitions and builds a container • When requested, creates objects and injects the dependencies
  29. ADVANCED HOW IT WORKS • Compiler Passes • Container Extensions

    • Services Configurator • Tagged Services
  30. SYMFONY DIC • Basic (and advances) usages are well documented

    in the Symfony official documentation • Probably you are comfortable creating your own objects via the container • So, let’s try to solve the problems we stated at the beginning
  31. final class OrderProcessor! {! private $orderRepository;! private $lockSystem;! private $objectStore;!

    ! function __construct(! ! ! OrderRepository $repository, ! ! ! LockSystem $lockSystem, ! ! ! ObjectStore $objectStore)! {! $this->orderRepository = $repository;! $this->lockSystem = $lockSystem;! $this->objectStore = $objectStore;! }! }! ! final class ObjectStore! {! function __construct(StorageDriver $driver)! {! }! }! ! ! interface StorageDriver ! {}! ! final class RiakStorageDriver implements StorageDriver! {}! ! final class RedisStorageDriver implements StorageDriver! {}! ! final class InMemoryStorageDriver implements StorageDriver! {}! ! interface LockSystem ! {}! ! final class RedisLockSystem implements LockSystem! {}! ! final class ZooKeeperLockSystem implements LockSystem! {}! interface OrderRepository {}! ! final class MySqlOrderRepository implements OrderRepository! {}! ! final class CassandralOrderRepository implements OrderRepository! {}!
  32. services:! ! order_processor:! ! ! class: OrderProcessor! arguments:! ! !

    ! object_store: @object_store! ! ! ! order_repository: [mysql|cassandra] ! ! ! ! lock_system: [redis|zookeeper]! ! ! ! ! ! ! object_store:! ! ! class: ObjectStore! ! ! arguments:! ! ! storage_driver: [riak|redis|inmemory]! MySqlOrderRepository CassandraOrderRepository RedisLockSystem ZooKeeperLockSystem RedisStorageDriver InMemoryStorageDriver RiakStorageDriver
  33. THE PROBLEM • It’s not possible to decide in advance

    what concrete implementation a deployment or environment is going to use • We must configure dependencies for all the concrete implementations and each one has their own dependencies
  34. SERVICES SETUP Deployment Environment Concrete Implementation DRAGON TESTING in-memory DRAGON

    STAGING/QA redis DRAGON PROD mysql MONSTER TESTING in-memory MONSTER PROD cassandra
  35. SOLUTIONS 1. Loading different configurations depending on the environment and

    aliasing services 2. Using synthetic services and aliases 3. Managing semantic configuration with extensions 4. Using a custom injector
  36. 1- LOADING DIFFERENT CONFIGURATIONS Load different configurations depending on the

    kernel.environment • Probably easy to setup for testing/dummy services Pros Cons • The choice is coupled to the environment • All services get defined, even when you are not going to use them
  37. ! namespace Acme\DemoBundle\DependencyInjection;! ! use Symfony\Component\DependencyInjection\ContainerBuilder;! use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;! use Symfony\Component\HttpKernel\DependencyInjection\Extension;!

    use Symfony\Component\Config\FileLocator;! ! class AcmeDemoExtension extends Extension! {! public function load(array $configs, ContainerBuilder $container)! {! $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));! $loader->load('services.yml');! ! if ($this->container->getParameter('kernel.environment') == 'test') {! $loader->load('services_test.yml');! }! }! ! public function getAlias()! {! return 'acme_demo';! }! }! services:! storage_driver:! alias: storage_driver.riak! ! storage_driver.riak:! class: RiakStorageDriver! ! storage_driver.redis:! class: RedisStorageDriver! ! storage_driver.memory:! class: InMemoryStorageDriver ! services:! storage_driver:! alias: storage_driver.memory! services.yml services_test.yml The choice is coupled to the environment
  38. 2- USING SYNTHETIC SERVICES AND ALIAS Define abstraction as “synthetic”

    services • Probably easy to setup for testing/dummy services Pros Cons • All services get defined, even when you are not going to use them • You have to define dependencies of services you probably not are going to use
  39. ! namespace Acme\DemoBundle\DependencyInjection;! ! use Symfony\Component\DependencyInjection\ContainerBuilder;! use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;! use Symfony\Component\HttpKernel\DependencyInjection\Extension;!

    use Symfony\Component\Config\FileLocator;! ! class AcmeDemoExtension extends Extension! {! public function load(array $configs, ContainerBuilder $container)! {! $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));! $loader->load('services.yml');! }! ! public function getAlias()! {! return 'acme_demo';! }! }! services:! storage_driver:! synthetic: true! ! storage_driver.riak:! class: RiakStorageDriver! ! storage_driver.redis:! class: RedisStorageDriver! ! storage_driver.memory:! class: InMemoryStorageDriver! ! services:! storage_driver:! alias: storage_driver.redis! services.yml app/config/config.yml ! services:! storage_driver:! alias: storage_driver.memory! app/config/config_test.yml
  40. 3- MANAGING SEMANTIC CONFIGURATIONS WITH EXTENSIONS • Instead of having

    the user override individual parameters, you let the user configure just a few, specifically created options. • As the bundle developer, you then parse through that configuration and load services inside an “Extension" • With this method, you won't need to import any configuration resources from your main application configuration: the Extension class can handle all of this
  41. 3- MANAGING SEMANTIC CONFIGURATIONS WITH EXTENSIONS • Good fit if

    you are building a bundle to be used by 3rd parties and you have a lot of configuration options you want to validate • Lot of boilerplate code • Extra complexity added • You just wanted to manage dependency injection right?
  42. class Configuration implements ConfigurationInterface! {! protected function addObjectStoreSection(ArrayNodeDefinition $rootNode)! {!

    $rootNode! ->children()! ->arrayNode('object_store')! ->isRequired()! ->children()! ->arrayNode('store_driver')! ->children()! ->scalarNode('type')->isRequired()->end()! ->scalarNode('connection')->end()! ->scalarNode('create_buckets')->end()! ->end()! ->validate()! ->ifTrue(function ($v) {! switch ($v['type']) {! case 'doctrine_dbal':! if (!isset($v['connection']) ||! !isset($v['create_buckets'])) {! return true;! }! break;! }! ! return false;! })! ->thenInvalid('children configuration')! ->end()! ->isRequired()! ->end()! ->end()! ->end();! }! } ! $definition = $container->getDefinition($storageDriver);! switch ($config['storage_driver']['type']) {! case 'doctrine_dbal':! $definition->replaceArgument(0, new Reference($config['storage_driver']['connection']));! $definition->replaceArgument(1, $config['storage_driver']['create_buckets']);! break;! case 'redis':! $definition->replaceArgument(0, new Reference($config['storage_driver']['connection']));! break;! }! ! $container->setAlias('object_storage.driver', $storageDriver);! Configuration Extension
  43. 3- MANAGING SEMANTIC CONFIGURATIONS WITH EXTENSIONS • More powerful than

    simply defining parameters: a specific option value might trigger the creation of many service definitions; • Ability to have configuration hierarchy • Smart merging of several config files (e.g. config_dev.yml and config.yml) Pros Cons • Too much verbose, a lot of boilerplate code • Extra complexity added if you only want to define dependencies and not a long configuration tree !
  44. 4- USING A CUSTOM INJECTOR • Let’s forget the Symfony

    DIC for a moment • Let’s go back to dependency management problem again
  45. services:! ! order_processor:! ! ! class: OrderProcessor! arguments:! ! !

    ! object_store: @object_store! ! ! ! order_repository: [mysql|cassandra] ! ! ! ! lock_system: [redis|zookeeper]! ! ! ! ! ! ! object_store:! ! ! class: ObjectStore! ! ! arguments:! ! ! storage_driver: [riak|redis|inmemory]! MySqlOrderRepository CassandraOrderRepository RedisLockSystem ZooKeeperLockSystem RedisStorageDriver InMemoryStorageDriver RiakStorageDriver
  46. “A good architecture allows you to defer critical decisions, it

    doesn’t force you to defer them. However, if you can defer them, it means you have lots of flexibility” ! Uncle Bob
  47. services:! ! order_processor:! ! ! class: OrderProcessor! arguments:! ! !

    ! object_store: @object_store! ! ! ! order_repository: [mysql|cassandra] ! ! ! ! lock_system: [redis|zookeeper]! ! ! ! ! ! ! object_store:! ! ! class: ObjectStore! ! ! arguments:! ! ! storage_driver: [riak|redis|inmemory]! MySqlOrderRepository CassandraOrderRepository RedisLockSystem ZooKeeperLockSystem RedisStorageDriver InMemoryStorageDriver RiakStorageDriver We need a dependency injection tool that allow us to easily change volatile decisions We need a dependency injection tool that allow us to defer critical decisions
  48. Core Framework Payment Component Analytics Component Object Store Lock System

    Component Exports Depends On Unit Of Work Storage Driver Order Processor Product Repository Order Repository Payment Gateway Tracker Tracker Queue Implementations Redis, Zookeper Redis, C*, Riak, MySql Config, API MySql, C* Itunes, Facebook, Amazon, Google Play Redis, RabbitMQ, SQS, Kinesis
  49. Core Component Object Store Lock System Component Exports Depends On

    Unit Of Work Storage Driver Implementations Redis, Zookeper Redis, C*, Riak, MySql namespace SP\Core\Game\Framework;! ! class FrameworkComponent implements Component! {! public function getName()! {! return 'core.game.framework';! }! ! public function getServicesDefinition()! {! return ServicesDefinition::create()! ! ->dependsOn('storage_driver')! ->withInstanceOf('SP\Core\Component\ObjectStore\Storage\StorageDriver')! ! ->dependsOn('lock_system')! ->withInstanceOf('SP\Core\Component\Lock\LockSystem')! ! ->exports('object_store')! ->withClass('SP\Core\Component\ObjectStore')! ->andConstructorDependencies(‘storage_driver’, ‘lock_system’);! }! } These are the decisions the component developer wants to defer Depends on abstractions, not concrete implementations
  50. Payment Component Order Processor Product Repository Order Repository Config, API

    MySql, C* namespace SP\Core\Game\Payment;! ! class PaymentComponent implements Component! {! public function getName()! {! return 'core.game.payment';! }! ! public function getServicesDefinition()! {! return ServicesDefinition::create()! ! ! ! ! ->dependsOn('product_repository')! ->withInstanceOf('SP\Core\Game\Payment\Product\Repository\ProductRepository')! ! ->dependsOn('order_repository')! ->withInstanceOf('SP\Core\Game\Payment\Order\Repository\OrderRepository')! ! ->dependsOn('gateways')! ->withInstanceOf('GatewayDefinition')! ! ->exports(‘order_processor')! ->withClass('SP\Core\Game\Payment\OrderProcessor')! ->andConstructorDependencies(‘gateways', ‘product_repository’, ‘order_repository’);! }! } Itunes, Facebook, Amazon, Google Play Payment Gateway These are the decisions the component developer wants to defer Depends on abstractions, not concrete implementations
  51. WHERE DOES THE MAGIC COME FROM? • There is not

    such “magic” • Each component define it’s dependencies in a easy and legible way • Framework agnostic dependency definition • Based on the dependency definitions, services are added to the container during the container building phase
  52. HOW? • First, collect services definitions from installed components •

    Second, inject services definitions into the Symfony (or Silex, or…) container • No magic, just code !!!
  53. USING SYMFONY • Collect services definitions from installed components •

    Inject services definitions into the Symfony (or Silex, or…) container
  54. use Symfony\Component\DependencyInjection as DI;! ! ! final class ComponentDependencyInjector implements

    DI\Compiler\CompilerPassInterface! {! private $components;! ! public function __construct(array $components = array())! {! $this->components = $components;! }! ! public function process(ContainerBuilder $container)! {! foreach ($this->components as $component) {! $services = $component->getServicesDefinition();! foreach ($services->dependencies as $definition) {! $id = $component->getName() . '.' . $definition->name;! ! if (!$container->has($id)) {! throw new ServiceNotFoundException($component->getName(), $id, $definition->instanceOf);! }! }! }! }! ! public function registerComponentsDependencies(ContainerBuilder $container)! {! foreach ($this->components as $component) {! $this->addComponentDependencies($container, $component);! }! }! ! private function addComponentDependencies(ContainerBuilder $container, Component $component)! {! $container->addObjectResource($component);! $services = $component->getServicesDefinition();! foreach ($services->exports as $definition) {! $this->addDefinition($container, $definition, $component);! }! foreach ($services->definitions as $definition) {! $this->addDefinition($container, $definition, $component);! }! }! ! $def = new DI\Definition($definition->class, $args);! $def->setPublic($definition->public);! ! $container->setDefinition($component->getName() . '.' . $definition->name, $def);! }! }! The Component Dependency Injector responsibility is to inject the definitions in a given container
  55. use Symfony\Component\HttpKernel\Kernel;! use Symfony\Component\Config\Loader\LoaderInterface;! ! class AppKernel extends Kernel! {!

    public function registerBundles()! {! $bundles = array(! new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),! new Symfony\Bundle\MonologBundle\MonologBundle(),! ! new SP\Core\Bundle\GameFrameworkBundle\GameFrameworkBundle([! new \SP\Core\Game\Framework\FrameworkComponent(),! new \SP\Core\Game\Framework\PaymentComponent(),! new \SP\Core\Game\Framework\AnalyticsComponent(),! ]),! );! }! }! The AppKernel These are the framework agnostic components that provide infrastructure and logic for the application
  56. namespace SP\Core\Bundle\GameFrameworkBundle;! ! use Symfony\Component\DependencyInjection\Compiler\PassConfig;! use Symfony\Component\HttpKernel\Bundle\Bundle;! use Symfony\Component\DependencyInjection\ContainerBuilder;! !

    use SP\Core\Bridge\Symfony\DependencyInjection\ComponentDependencyInjector;! ! class GameFrameworkBundle extends Bundle! {! private $components;! ! function __construct(array $components = array())! {! $this->components = $components;! }! ! public function build(ContainerBuilder $container)! {! $injector = new ComponentDependencyInjector($this->components);! $injector->registerComponentsDependencies($container);! ! $container->addCompilerPass($injector, PassConfig::TYPE_BEFORE_REMOVING);! }! }! The (in)Famous Framework Bundle The components we are “installing” in this application The “magic” is here !!!
  57. [SP\Core\Bridge\Symfony\DependencyInjection\Exception\ServiceNotFoundException] Component "core.game.framework" requires the service "core.game.framework.lock_system" as an implementation

    of "SP\Core\Component\Lock \LockSystem" but the service is not defined. [SP\Core\Bridge\Symfony\DependencyInjection\Exception\ServiceNotFoundException] Component "core.game.payment" requires the service “core.game.payment.product_repository” as an implementation of "ProductRepository" but the service is not defined. $ php app/console The application complains about missing dependencies required by installed components We are planning to add suggested implementations for this requirement
  58. services:! ! ! core.game.framework.lock_system:! ! ! public: false! ! !

    class: SP\Core\Component\Lock\RedisLockSystem! ! ! arguments:! redis: @sp.core.redis.connector.lock! timeout: 10! expiration: 10! config.yml services:! ! ! core.game.framework.lock_system:! ! ! public: false! ! ! class: SP\Core\Component\Lock\MemoryLockSystem! ! ! arguments:! timeout: 10! expiration: 10! config_test.yml The application config Concrete implementation in the application (by default) Semantic configuration? Concrete implementation in the application (test mode)
  59. 4- USING A CUSTOM INJECTOR • Complete control of component

    dependencies • Very limited in features (only constructor injections allowed) • Framework agnostic • Allows you to defer critical and volatile decisions Pros Cons • Very limited in features (only constructor injections allowed) ! !
  60. CONCLUSIONS • You have the tools, use the best tool

    that solve your problems • Don’t be afraid of going to the limit with the framework you use • The framework is just an implementation detail
  61. SOME REFERENCES http://www.objectmentor.com/resources/articles/dip.pdf ! http://www.martinfowler.com/articles/injection.html ! http://martinfowler.com/articles/dipInTheWild.html ! http://desymfony.com/ponencia/2013/inyeccion-dependencias-aplicaciones-php !

    http://fabien.potencier.org/article/11/what-is-dependency-injection ! http://fabien.potencier.org/article/12/do-you-need-a-dependency-injection-container ! http://fabien.potencier.org/article/13/introduction-to-the-symfony-service-container ! https://www.youtube.com/watch?v=IKD2-MAkXyQ ! http://richardmiller.co.uk/2014/03/12/avoiding-setter-injection/ ! http://richardmiller.co.uk/2014/03/28/symfony2-configuring-different-services-for-different-environments/ ! http://www.infoq.com/news/2013/07/architecture_intent_frameworks ! http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html !