dependency injection with php and php 5.3
DESCRIPTION
TRANSCRIPT
Dependency Injection with PHP and PHP 5.3
Fabien Potencier
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
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
Fabien Potencier
• Read my technical blog: http://fabien.potencier.org/
• Follow me on Twitter: @fabpot
• Fork my code on Github: http://github.com/fabpot/
Dependency Injection
A real world « web » example
In most web applications, you need to manage the user preferences
– The user language – Whether the user is authenticated or not – The user credentials – …
This can be done with a User object
– setLanguage(), getLanguage() – setAuthenticated(), isAuthenticated() – addCredential(), hasCredential() – ...
The User information need to be persisted
between HTTP requests
We use the PHP session for the Storage
class SessionStorage { function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); }
function set($key, $value) { $_SESSION[$key] = $value; }
// ... }
class User { protected $storage;
function __construct() { $this->storage = new SessionStorage(); }
function setLanguage($language) { $this->storage->set('language', $language); }
// ... }
$user = new User();
Very easy to use
Very hard to
customize
class User { protected $storage;
function __construct($storage) { $this->storage = $storage; } }
$storage = new SessionStorage(); $user = new User($storage);
Slightly more
difficult to use
Very easy to
customize
That’s Dependency Injection
Nothing more
Let’s understand why the first example is not a good idea
I want to change the session cookie name
class User { protected $storage;
function __construct() { $this->storage = new SessionStorage('SESSION_ID'); }
function setLanguage($language) { $this->storage->set('language', $language); }
// ... }
$user = new User();
Hardcode it in the
User class
class User { protected $storage;
function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } }
define('STORAGE_SESSION_NAME', 'SESSION_ID');
$user = new User();
Add a global
configuration?
class User { protected $storage;
function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } }
$user = new User('SESSION_ID');
Configure via
User?
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?
I want to change the session storage implementation
Filesystem MySQL
Memcached …
class User { protected $storage;
function __construct() { $this->storage = Registry::get('session_storage'); } }
$storage = new SessionStorage(); Registry::set('session_storage', $storage); $user = new User();
Use a global
registry object?
Now, the User depends on the Registry
Instead of harcoding the Storage dependency
inside the User class constructor
Inject the Storage dependency in the User object
class User { protected $storage;
function __construct($storage) { $this->storage = $storage; } }
$storage = new SessionStorage('SESSION_ID'); $user = new User($storage);
What are the advantages?
Use different Storage strategies
class User { protected $storage;
function __construct($storage) { $this->storage = $storage; } }
$storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);
Use a different
Storage engine
Configuration becomes natural
class User { protected $storage;
function __construct($storage) { $this->storage = $storage; } }
$storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);
Configuration
is natural
Wrap third-party classes (Interface / Adapter)
class User { protected $storage;
function __construct(SessionStorageInterface $storage) { $this->storage = $storage; } }
interface SessionStorageInterface { function get($key);
function set($key, $value); }
Add an interface
Mock the Storage object (for testing)
class User { protected $storage;
function __construct(SessionStorageInterface $storage) { $this->storage = $storage; } }
class SessionStorageForTests implements SessionStorageInterface { protected $data = array();
static function set($key, $value) { self::$data[$key] = $value; } }
Mock the Session
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
« Dependency Injection is where components are given their dependencies
through their constructors, methods, or directly into fields. »
http://www.picocontainer.org/injection.html
$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;
A slightly more complex web example
$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);
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();
Back to square 1
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();
We need a Container
Describes objects and their dependencies
Instantiates and configures objects on-demand
A container SHOULD be able to manage
any PHP object (POPO)
The objects MUST not know that they are managed
by a container
• Parameters – The SessionStorageInterface implementation we want to use (the class name) – The session name
• Objects – SessionStorage – User
• Dependencies – User depends on a SessionStorageInterface implementation
Let’s build a simple container with PHP 5.3
DI Container
Managing parameters
class Container { protected $parameters = array();
public function setParameter($key, $value) { $this->parameters[$key] = $value; }
public function getParameter($key) { return $this->parameters[$key]; } }
$container = new Container(); $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);
Decoupling
Customization
Objects creation
class Container { protected $parameters = array();
public function __set($key, $value) { $this->parameters[$key] = $value; }
public function __get($key) { return $this->parameters[$key]; } }
Using PHP
magic methods
$container = new Container(); $container->session_name = 'SESSION_ID'; $container->storage_class = 'SessionStorage';
$sessionStorage = new $container->storage_class($container->session_name); $user = new User($sessionStorage);
Interface
is cleaner
DI Container
Managing objects
We need a way to describe how to create objects, without actually instantiating anything!
Anonymous functions to the rescue!
Anonymous Functions / Lambdas
A lambda is a function defined on the fly
with no name
function () { echo 'Hello world!'; };
Anonymous Functions / Lambdas
A lambda can be stored in a variable
$hello = function () { echo 'Hello world!'; };
Anonymous Functions / Lambdas
And then it can be used as any other PHP callable
$hello();
call_user_func($hello);
Anonymous Functions / Lambdas
You can also pass a lambda as an argument to a function or method
function foo(Closure $func) { $func(); }
foo($hello);
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');
DI Container
Managing objects
class Container { protected $parameters = array(); protected $objects = array();
public function __set($key, $value) { $this->parameters[$key] = $value; }
public function __get($key) { return $this->parameters[$key]; }
public function setService($key, Closure $service) { $this->objects[$key] = $service; }
public function getService($key) { return $this->objects[$key]($this); } }
Store a lambda
able to create the
object on-demand
Ask the closure to create
the object and pass the
current Container
$container = new Container(); $container->session_name = 'SESSION_ID'; $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); });
$user = $container->getService('user');
Creating the User
is now as easy as before
Description
class Container { protected $values = array();
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]; } } }
Simplify the code
$container = new Container(); $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;
Unified interface
DI Container
Scope
For some objects, like the user, the container must always return the same instance
spl_object_hash($container->user)
!== spl_object_hash($container->user)
$container->user = function ($c) { static $user;
if (is_null($user)) { $user = new User($c->storage); }
return $user; };
spl_object_hash($container->user)
=== spl_object_hash($container->user)
$container->user = $container->asShared(function ($c) { return new User($c->storage); });
A closure is a lambda that remembers the context
of its creation…
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'), );
$mapper = function ($article) { return $article->getTitle(); };
$titles = array_map($mapper, $articles);
$method = 'getTitle';
$mapper = function ($article) use($method) { return $article->$method(); };
$method = 'getAuthor';
$titles = array_map($mapper, $articles);
$mapper = function ($method) { return function ($article) use($method) { return $article->$method(); }; };
$titles = array_map($mapper('getTitle'), $articles);
$authors = array_map($mapper('getAuthor'), $articles);
$container->user = $container->asShared(function ($c) { return new User($c->storage); });
function asShared(Closure $lambda) { return function ($container) use ($lambda) { static $object;
if (is_null($object)) { $object = $lambda($container); } return $object; }; }
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]; } } }
Error management
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) { return function ($c) use ($callable) { static $object;
if (is_null($object)) { $object = $callable($c); } return $object; }; } }
40 LOC for a fully-
featured container
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/
http://github.com/fabpot/pimple
http://twittee.org/
Twittee: A Container in a tweet
• Implementation does not use PHP 5.3 • Its usage needs PHP 5.3
class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } }
twittee.org
Remember, most of the time, you don’t need a Container
to use Dependency Injection
You can start to use and benefit from Dependency Injection today
by implementing it in your projects
by using externals libraries that already use DI
without the need of a container
Symfony Zend Framework ezComponents
Doctrine Swift Mailer
…
Questions?
My slides will be available on slideshare.com/fabpot
with Matthew Weier O’Pheinney
I will reveal the first alpha release of Symfony 2.0!
symfony-live.com
with Matthew Weier O’Pheinney
… with Matthew Weier O'Phinney as a special guest
symfony-live.com
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/
Symfony Components Dependency Injection
BONUS
Symfony\Components\DependencyInjection
use Symfony\Components\DependencyInjection\Builder; use Symfony\Components\DependencyInjection\Reference;
$container = new Builder();
$container->register('output', 'FancyOutput');
$container-> register('message', 'Message')-> setArguments(array(new Reference('output'), array('with_newline' => true))) ;
$container->message->say('Hello World!');
DI Container Hello World example
Get the configuration for the message service
The Message constructor must be given an output service
Get the output object from the container
Create a Message object by passing the constructor arguments
$message = $container->message;
$message = $container->message;
is roughly equivalent to
$output = new FancyOutput(); $message = new Message($output, array('with_newline' => true));!
$container = new Builder();
$container->register('output', 'FancyOutput'); $container-> register('message', 'Message')-> setArguments(array(new Reference('output'), array('with_newline' => true))) ;
$container->message->say('Hello World!');
<container xmlns="http://symfony-project.org/2.0/container"> <services> <service id="output" class="FancyOutput" />
<service id="message" class="Message"> <argument type="service" id="output" /> <argument type="collection"> <argument key="with_newline">true</argument> </argument> </service> </services> </container>
$container = new Builder(); $loader = new XmlFileLoader($container); $loader->load('services.xml');
PHP
XML XML is validated against an XSD
$container = new Builder();
$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 } message: class: Message arguments: - @output - { with_newline: true }
$container = new Builder(); $loader = new YamlFileLoader($container); $loader->load('services.yml');
PHP
YAML
<container xmlns="http://symfony-project.org/2.0/container"> <parameters> <parameter key="output.class">FancyOutput</parameter> <parameter key="message.options" type="collection"> <parameter key="with_newline">true</parameter> </parameter> </parameters>
<services> <service id="output" class="%output.class%" />
<service id="message" class="Message"> <argument type="service" id="output" /> <argument>%message.options%</argument> </service> </services> </container>
$container = new Builder(); $loader = new XmlFileLoader($container); $loader->load('services.xml');
<container xmlns="http://symfony-project.org/2.0/container"> <imports> <import resource="config.xml" /> </imports>
<services> <service id="output" class="%output.class%" /> <service id="message" class="Message"> <argument type="service" id="output" /> <argument>%message.options%</argument> </service> </services> </container>
<container xmlns="http://symfony-project.org/2.0/container"> <parameters> <parameter key="output.class">FancyOutput</parameter> <parameter key="message.options" type="collection"> <parameter key="with_newline">true</parameter> </parameter> </parameters> </container>
$container = new Builder(); $loader = new FileXmlFileLoader($container); $loader->load('services.xml');
<services> <import resource="config.yml" class="Symfony\Components\DependencyInjection\Loader\YamlFileLoader" />
<service id="output" class="%output.class%" /> <service id="message" class="Message"> <argument type="service" id="output" /> <argument>%message.options%</argument> </service> </services>
parameters: output.class: FancyOutput
message.options: { with_newline: true }
$container = new Builder(); $loader = new XmlFileLoader($container); $loader->load('services.xml');
Loaders & Dumpers
• IniFileLoader • XmlFileLoader • YamlFileLoader
• XmlDumper • YamlDumper • PhpDumper • GraphvizDumper
Make your container VERY fast
use Symfony\Components\DependencyInjection\Builder; use Symfony\Components\DependencyInjection\Reference; use Symfony\Components\DependencyInjection\Dumper\XmlDumper; use Symfony\Components\DependencyInjection\Dumper\YamlDumper; use Symfony\Components\DependencyInjection\Dumper\PhpDumper; use Symfony\Components\DependencyInjection\Loader\XmlFileLoader; use Symfony\Components\DependencyInjection\Loader\YamlFileLoader;
$container = new Builder();
$container->register('output', 'FancyOutput');
$container-> register('message', 'Message')-> setArguments(array(new Reference('output'), array('with_newline' => true))) ;
$dumper = new XmlDumper($container); file_put_contents(__DIR__.'/container.xml', $dumper->dump());
$loader = new XmlFileLoader($container); $loader->load(__DIR__.'/container.xml');
$dumper = new YamlDumper($container); file_put_contents(__DIR__.'/container.yml', $dumper->dump());
$loader = new YamlFileLoader($container); $loader->load(__DIR__.'/container.yml');
$dumper = new PhpDumper($container); echo $dumper->dump();
use Symfony\Components\DependencyInjection\Container; use Symfony\Components\DependencyInjection\Reference; use Symfony\Components\DependencyInjection\Parameter;
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->getService('output'), array('with_newline' => true));
return $this->shared['message'] = $instance; } }
use Symfony\Components\DependencyInjection\Dumper\GraphvizDumper;
$dumper = new GraphvizDumper($container); echo $dumper->dump();
digraph sc { ratio="compress" node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"];
node_output [label="output\nFancyOutput\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_message [label="message\nMessage\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_service_container [label="service_container\nSymfony\\Components\\DependencyInjection\\Builder\n", shape=record, fillcolor="#9999ff", style="filled"]; node_message -> node_output [label="" style="filled"]; }