Dependency Injection with PHP 5.3

50,478 views
46,802 views

Published on

My presentation about Dependency Injection with PHP 5.3 (UPDATED)

Published in: Technology, Business
1 Comment
87 Likes
Statistics
Notes
  • If we pass into session_name 'SESSION_ID' we have an exception with message PHP Warning: session_id() expects parameter 1 to be string, object given, cause SESSION_ID is a function in php code, and is_callable return true. And we call it by pass $this session_id($this) in __get() method of Container
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total views
50,478
On SlideShare
0
From Embeds
0
Number of Embeds
4,599
Actions
Shares
0
Downloads
639
Comments
1
Likes
87
Embeds 0
No embeds

No notes for slide

Dependency Injection with PHP 5.3

  1. Dependency Injection with PHP 5.3 Fabien Potencier
  2. Fabien Potencier •  Serial entrepreneur and developer by passion •  Founder of Sensio (in 1998) –  A services and consulting company specialized in Web technologies and Internet marketing (France and USA) –  70 people –  Open-Source specialists –  Big corporate customers –  Consulting, training, development, web design, … and more –  Sponsor of a lot of Open-Source projects like symfony and Doctrine
  3. Fabien Potencier •  Creator and lead developer of symfony… •  and creator and lead developer of some more OSS: –  symfony components –  Swift Mailer : Powerful component based mailing library for PHP –  Twig : Fexible, fast, and secure template language for PHP –  Pirum : Simple PEAR Channel Server Manager –  Sismo : PHP continuous integration server –  Lime : Easy to use unit testing library for PHP –  Twitto : A web framework in a tweet –  Twittee : A Dependency Injection Container in a tweet –  Pimple : A small PHP 5.3 dependency injection container
  4. Fabien Potencier •  Read my technical blog: http://fabien.potencier.org/ •  Follow me on Twitter: @fabpot •  Fork my code on Github: http://github.com/fabpot/
  5. Dependency Injection A real world « web » example
  6. In most web applications, you need to manage the user preferences –  The user language –  Whether the user is authenticated or not –  The user credentials –  …
  7. This can be done with a User object –  setLanguage(), getLanguage() –  setAuthenticated(), isAuthenticated() –  addCredential(), hasCredential() –  ...
  8. The User information need to be persisted between HTTP requests We use the PHP session for the Storage
  9. class SessionStorage { function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); } function set($key, $value) { $_SESSION[$key] = $value; } // ... }
  10. class User { protected $storage; function __construct() Very hard to { customize $this->storage = new SessionStorage(); } function setLanguage($language) { $this->storage->set('language', $language); } // ... } Very easy to use $user = new User();
  11. class User { protected $storage; Very easy to customize function __construct($storage) { $this->storage = $storage; } } $storage = new SessionStorage(); $user = new User($storage); Slightly more difficult to use
  12. That’s Dependency Injection Nothing more
  13. Let’s understand why the first example is not a good idea
  14. I want to change the session cookie name
  15. class User { protected $storage; function __construct() Hardcode it in the { User class $this->storage = new SessionStorage('SESSION_ID'); } function setLanguage($language) { $this->storage->set('language', $language); } // ... } $user = new User();
  16. class User { protected $storage; function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } } Add a global configuration? define('STORAGE_SESSION_NAME', 'SESSION_ID'); $user = new User();
  17. class User { protected $storage; function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } } $user = new User('SESSION_ID'); Configure via User?
  18. class User { protected $storage; function __construct($storageOptions) { $this->storage = new SessionStorage($storageOptions ['session_name']); $user = new User( array('session_name' => 'SESSION_ID') ); Configure with an array?
  19. I want to change the session storage implementation Filesystem MySQL Memcached …
  20. class User { protected $storage; function __construct() Use a global { registry object? $this->storage = Registry::get('session_storage'); } } $storage = new SessionStorage(); Registry::set('session_storage', $storage); $user = new User();
  21. Now, the User depends on the Registry
  22. Instead of harcoding the Storage dependency inside the User class constructor Inject the Storage dependency in the User object
  23. class User { protected $storage; function __construct($storage) { $this->storage = $storage; } } $storage = new SessionStorage('SESSION_ID'); $user = new User($storage);
  24. What are the advantages?
  25. Use different Storage strategies
  26. class User { protected $storage; function __construct($storage) { $this->storage = $storage; } } Use a different Storage engine $storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);
  27. Configuration becomes natural
  28. class User { protected $storage; function __construct($storage) { $this->storage = $storage; } } Configuration is natural $storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);
  29. Wrap third-party classes (Interface / Adapter)
  30. class User { protected $storage; Add an interface function __construct(SessionStorageInterface $storage) { $this->storage = $storage; } } interface SessionStorageInterface { function get($key); function set($key, $value); }
  31. Mock the Storage object (for testing)
  32. class User { protected $storage; function __construct(SessionStorageInterface $storage) { $this->storage = $storage; } } Mock the Session class SessionStorageForTests implements SessionStorageInterface { protected $data = array(); static function set($key, $value) { self::$data[$key] = $value; } }
  33. Use different Storage strategies Configuration becomes natural Wrap third-party classes (Interface / Adapter) Mock the Storage object (for testing) Easy without changing the User class
  34. « Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields. » http://www.picocontainer.org/injection.html
  35. $storage = new SessionStorage(); // constructor injection $user = new User($storage); // setter injection $user = new User(); $user->setStorage($storage); // property injection $user = new User(); $user->storage = $storage;
  36. A slightly more complex web example
  37. $request = new Request(); $response = new Response(); $storage = new FileSessionStorage('SESSION_ID'); $user = new User($storage); $cache = new FileCache( array('dir' => dirname(__FILE__).'/cache') ); $routing = new Routing($cache);
  38. class Application { function __construct() { $this->request = new WebRequest(); $this->response = new WebResponse(); $storage = new FileSessionStorage('SESSION_ID'); $this->user = new User($storage); $cache = new FileCache( array('dir' => dirname(__FILE__).'/cache') ); $this->routing = new Routing($cache); } } $application = new Application();
  39. Back to square 1
  40. class Application { function __construct() { $this->request = new WebRequest(); $this->response = new WebResponse(); $storage = new FileSessionStorage('SESSION_ID'); $this->user = new User($storage); $cache = new FileCache( array('dir' => dirname(__FILE__).'/cache') ); $this->routing = new Routing($cache); } } $application = new Application();
  41. We need a Container Describes objects and their dependencies Instantiates and configures objects on-demand
  42. A container SHOULD be able to manage ANY PHP object (POPO) The objects MUST not know that they are managed by a container
  43. •  Parameters –  The SessionStorageInterface implementation we want to use (the class name) –  The session name •  Objects –  SessionStorage –  User •  Dependencies –  User depends on a SessionStorageInterface implementation
  44. Let’s build a simple container with PHP 5.3
  45. DI Container Managing parameters
  46. class Container { protected $parameters = array(); public function setParameter($key, $value) { $this->parameters[$key] = $value; } public function getParameter($key) { return $this->parameters[$key]; } }
  47. Decoupling $container = new Container(); Customization $container->setParameter('session_name', 'SESSION_ID'); $container->setParameter('storage_class', 'SessionStorage'); $class = $container->getParameter('storage_class'); $sessionStorage = new $class($container->getParameter('session_name')); $user = new User($sessionStorage); Objects creation
  48. class Container Using PHP { magic methods protected $parameters = array(); public function __set($key, $value) { $this->parameters[$key] = $value; } public function __get($key) { return $this->parameters[$key]; } }
  49. Interface is cleaner $container = new Container(); $container->session_name = 'SESSION_ID'; $container->storage_class = 'SessionStorage'; $sessionStorage = new $container->storage_class($container->session_name); $user = new User($sessionStorage);
  50. DI Container Managing objects
  51. We need a way to describe how to create objects, without actually instantiating anything! Anonymous functions to the rescue!
  52. Anonymous Functions / Lambdas A lambda is a function defined on the fly with no name function () { echo 'Hello world!'; };
  53. Anonymous Functions / Lambdas A lambda can be stored in a variable $hello = function () { echo 'Hello world!'; };
  54. Anonymous Functions / Lambdas And then it can be used as any other PHP callable $hello(); call_user_func($hello);
  55. Anonymous Functions / Lambdas You can also pass a lambda as an argument to a function or method function foo(Closure $func) { $func(); } foo($hello);
  56. Fonctions anonymes $hello = function ($name) { echo 'Hello '.$name; }; $hello('Fabien'); call_user_func($hello, 'Fabien'); function foo(Closure $func, $name) { $func($name); } foo($hello, 'Fabien');
  57. DI Container Managing objects
  58. class Container { protected $parameters = array(); protected $objects = array(); public function __set($key, $value) { $this->parameters[$key] = $value; } public function __get($key) Store a lambda { return $this->parameters[$key]; able to create the object on-demand } public function setService($key, Closure $service) { $this->objects[$key] = $service; } public function getService($key) Ask the closure to create { return $this->objects[$key]($this); th e object and pass the } } current Container
  59. $container = new Container(); $container->session_name = 'SESSION_ID'; Description $container->storage_class = 'SessionStorage'; $container->setService('user', function ($c) { return new User($c->getService('storage')); }); $container->setService('storage', function ($c) { return new $c->storage_class($c->session_name); }); Creating the User is now as easy as before $user = $container->getService('user');
  60. class Container { protected $values = array(); Simplify the code function __set($id, $value) { $this->values[$id] = $value; } function __get($id) { if (is_callable($this->values[$id])) { return $this->values[$id]($this); } else { return $this->values[$id]; } } }
  61. $container = new Container(); Unified interface $container->session_name = 'SESSION_ID'; $container->storage_class = 'SessionStorage'; $container->user = function ($c) { return new User($c->storage); }; $container->storage = function ($c) { return new $c->storage_class($c->session_name); }; $user = $container->user;
  62. DI Container Scope
  63. For some objects, like the user, the container must always return the same instance
  64. spl_object_hash($container->user) !== spl_object_hash($container->user)
  65. $container->user = function ($c) { static $user; if (is_null($user)) { $user = new User($c->storage); } return $user; };
  66. spl_object_hash($container->user) === spl_object_hash($container->user)
  67. $container->user = $container->asShared(function ($c) { return new User($c->storage); });
  68. A closure is a lambda that remembers the context of its creation…
  69. class Article { public function __construct($title) { $this->title = $title; } public function getTitle() { return $this->title; } } $articles = array( new Article('Title 1'), new Article('Title 2'), );
  70. $mapper = function ($article) { return $article->getTitle(); }; $titles = array_map($mapper, $articles);
  71. $method = 'getTitle'; $mapper = function ($article) use($method) { return $article->$method(); }; $method = 'getAuthor'; $titles = array_map($mapper, $articles);
  72. $mapper = function ($method) { return function ($article) use($method) { return $article->$method(); }; };
  73. $titles = array_map($mapper('getTitle'), $articles); $authors = array_map($mapper('getAuthor'), $articles);
  74. $container->user = $container->asShared(function ($c) { return new User($c->storage); });
  75. function asShared(Closure $lambda) { return function ($container) use ($lambda) { static $object; if (is_null($object)) { $object = $lambda($container); } return $object; }; }
  76. class Container { protected $values = array(); function __set($id, $value) { $this->values[$id] = $value; } function __get($id) { Error management if (!isset($this->values[$id])) { throw new InvalidArgumentException(sprintf('Value "%s" is not defined.', $id)); } if (is_callable($this->values[$id])) { return $this->values[$id]($this); } else { return $this->values[$id]; } } }
  77. class Container { protected $values = array(); function __set($id, $value) { $this->values[$id] = $value; } function __get($id) { if (!isset($this->values[$id])) { throw new InvalidArgumentException(sprintf('Value "%s" is not defined.', $id)); } if (is_callable($this->values[$id])) { return $this->values[$id]($this); } else { return $this->values[$id]; } } function asShared($callable) { 40 LOC for a fully- return function ($c) use ($callable) { static $object; if (is_null($object)) { $object = $callable($c); featured container } return $object; }; } }
  78. I’m NOT advocating the usage of lambdas everywhere This presentation is about showing how they work on practical examples
  79. A DI Container does NOT manage ALL your objects
  80. Good rule of thumb: It manages “Global” objects Objects with only one instance (!= Singletons)
  81. LIKE a User, a Request, a database Connection, a Logger, …
  82. UNLIKE Model objects (a Product, a blog Post, …)
  83. Symfony Components Dependency Injection
  84. Rock-solid implementation of a DIC in PHP 5.3
  85. At the core of the Symfony 2.0 framework … which is the fastest framework for a “Hello World” app (about 4-5 times faster than symfony 1.4)
  86. Very flexible Configuration in PHP, XML, YAML, or INI
  87. $container = new Builder(); PHP $container->register('output', 'FancyOutput'); $container-> register('message', 'Message')-> setArguments(array(new sfServiceReference('output'), array('with_newline' => true))) ; $container->message->say('Hello World!'); services: output: { class: FancyOutput } YAML message: class: Message arguments: - @output - { with_newline: true } <container xmlns="http://symfony-project.org/schema/dic/services"> <services> <service id="output" class="FancyOutput" /> XML <service id="message" class="Message"> <argument type="service" id="output" /> <argument type="collection"> <argument key="with_newline">true</argument> </argument> </service> </services> </container>
  88. <container xmlns="http://symfony-project.org/schema/dic/services"> <import resource="parameters.yml" /> <import resource="parameters.ini" /> <import resource="services.xml" /> </container> imports: - { resource: parameters.yml } - { resource: parameters.ini } - { resource: services.xml }
  89. Fast as hell The container can be “compiled” down to plain PHP code
  90. use SymfonyComponentsDependencyInjectionContainer; use SymfonyComponentsDependencyInjectionReference; use SymfonyComponentsDependencyInjectionParameter; class ProjectServiceContainer extends Container { protected $shared = array(); protected function getOutputService() { if (isset($this->shared['output'])) return $this->shared['output']; $instance = new FancyOutput(); return $this->shared['output'] = $instance; } protected function getMessageService() { if (isset($this->shared['message'])) return $this->shared['message']; $instance = new Message($this->getOutputService(), array('with_newline' => true)); return $this->shared['message'] = $instance; } }
  91. Semantic configuration Thanks to an extension mechanism
  92. <container xmlns="http://www.symfony-project.org/schema/dic/services"> <zend:logger priority="debug" path="%kernel.root_dir%/logs/%kernel.environment%.log" /> <doctrine:dbal dbname="dbname" username="root" password="" /> <swift:mailer transport="gmail"> <swift:username>fabien.potencier</swift:username> <swift:password>xxxxxx</swift:password> </swift:mailer> </container>
  93. <container xmlns="http://www.symfony-project.org/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:swift="http://www.symfony-project.org/schema/dic/swiftmailer" xmlns:doctrine="http://www.symfony-project.org/schema/dic/doctrine" xmlns:zend="http://www.symfony-project.org/schema/dic/zend" xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony- project.org/schema/dic/services/services-1.0.xsd http://www.symfony-project.org/schema/dic/doctrine http://www.symfony- project.org/schema/dic/doctrine/doctrine-1.0.xsd http://www.symfony-project.org/schema/dic/swiftmailer http:// www.symfony-project.org/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> <zend:logger priority="debug" path="%kernel.root_dir%/logs/%kernel.environment%.log" /> auto-completion <doctrine:dbal dbname="dbname" username="root" password="" /> <swift:mailer transport="gmail"> and validation <swift:username>fabien.potencier</swift:username> <swift:password>xxxxxx</swift:password> with XSD </swift:mailer> </container>
  94. zend.logger: level: debug path: %kernel.root_dir%/logs/%kernel.environment%.log doctrine.dbal: dbname: dbname username: root password: ~ swift.mailer: transport: gmail username: fabien.potencier password: xxxxxxx
  95. Everything is converted by the extension to plain services and parameters no overhead
  96. Loader::registerExtension(new SwiftMailerExtension()); Loader::registerExtension(new DoctrineExtension()); Loader::registerExtension(new ZendExtension()); $loader = new XmlFileLoader(__DIR__); $config = $loader->load('services.xml'); $container = new Builder(); $container->merge($config); $container->mailer->... $dumper = new PhpDumper($container); echo $dumper->dump();
  97. More about Dependency Injection http://fabien.potencier.org/article/17/on-php-5-3-lambda-functions-and-closures http://components.symfony-project.org/dependency-injection/ (5.2) http://github.com/fabpot/symfony/tree/master/src/Symfony/Components/DependencyInjection/(5.3) http://github.com/fabpot/pimple http://twittee.org/
  98. Remember, most of the time, you don’t need a Container to use Dependency Injection
  99. You can start to use and benefit from Dependency Injection today
  100. by implementing it in your projects by using externals libraries that already use DI without the need of a container
  101. Symfony Zend Framework ezComponents Doctrine Swift Mailer …
  102. symfony-live.com with Matthew Weier O’Pheinney I will reveal the first alpha release of Symfony 2.0!
  103. Questions? My slides will be available on slideshare.com/fabpot
  104. Sensio S.A. 92-98, boulevard Victor Hugo 92 115 Clichy Cedex FRANCE Tél. : +33 1 40 99 80 80 Contact Fabien Potencier fabien.potencier at sensio.com http://www.sensiolabs.com/ http://www.symfony-project.org/ http://fabien.potencier.org/

×