Dependency injection in PHP 5.3/5.4
- 3. In most web applications, you need to manage the user preferences
–The user language
–Whether the user is authenticated or not
–The user credentials
–…
- 4. This can be done with a User object
–setLanguage(), getLanguage()
–setAuthenticated(), isAuthenticated()
–addCredential(), hasCredential()
–...
- 5. The User information
need to be persisted
between HTTP requests
We use the PHP session for the Storage
- 6. class SessionStorage
{
function __construct($cookieName = 'PHP_SESS_ID')
{
session_name($cookieName);
session_start();
}
function set($key, $value)
{
$_SESSION[$key] = $value;
}
// ...
}
- 7. 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();
- 8. 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. class User
{
protected $storage;
H ardcode it in the
function __construct() User class
{
$this->storage = new SessionStorage('SESSION_ID');
}
function setLanguage($language)
{
$this->storage->set('language', $language);
}
// ...
}
$user = new User();
- 13. 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();
- 14. class User
{
protected $storage;
function __construct($sessionName)
{
$this->storage = new SessionStorage($sessionName);
}
}
$user = new User('SESSION_ID');
Configure via
User?
- 15. 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?
- 16. I want to change the session storage implementation
Filesystem
MySQL
Memcached
…
- 17. 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();
- 19. Instead of harcoding
the Storage dependency
inside the User class constructor
Inject the Storage dependency
in the User object
- 20. class User
{
protected $storage;
function __construct($storage)
{
$this->storage = $storage;
}
}
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);
- 23. class User
{
protected $storage;
function __construct($storage)
{
$this->storage = $storage;
} Use a different
} Storage engine
$storage = new MySQLSessionStorage('SESSION_ID');
$user = new User($storage);
- 25. class User
{
protected $storage;
function __construct($storage)
{
$this->storage = $storage;
} Configuration
} is natural
$storage = new MySQLSessionStorage('SESSION_ID');
$user = new User($storage);
- 27. class User
{
protected $storage; Add an interface
function __construct(SessionStorageInterface $storage)
{
$this->storage = $storage;
}
}
interface SessionStorageInterface
{
function get($key);
function set($key, $value);
}
- 29. 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;
}
}
- 30. 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
- 31. « Dependency Injection is where
components are given their dependencies
through their constructors, methods, or
directly into fields. »
http://www.picocontainer.org/injection.html
- 32. $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;
- 34. $request = new Request();
$response = new Response();
$storage = new FileSessionStorage('SESSION_ID');
$user = new User($storage);
$cache = new FileCache(
array('dir' => __DIR__.'/cache')
);
$routing = new Routing($cache);
- 35. 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();
- 37. 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();
- 38. We need a Container
Describes objects
and their dependencies
Instantiates and configures
objects on-demand
- 39. A container
SHOULD be able to manage
ANY PHP object (POPO)
The objects MUST not know
that they are managed
by a container
- 40. • Parameters
–The SessionStorageInterface implementation we want to use (the class name)
–The session name
• Objects
–SessionStorage
–User
• Dependencies
–User depends on a SessionStorageInterface implementation
- 43. class Container
{
protected $parameters = array();
public function setParameter($key, $value)
{
$this->parameters[$key] = $value;
}
public function getParameter($key)
{
return $this->parameters[$key];
}
}
- 44. 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
- 45. Using PHP
magic methods
class Container
{
protected $parameters = array();
public function __set($key, $value)
{
$this->parameters[$key] = $value;
}
public function __get($key)
{
return $this->parameters[$key];
}
}
- 46. 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);
- 48. We need a way to describe how to create objects,
without actually instantiating anything!
Anonymous functions to the rescue!
- 49. A lambda is a function
defined on the fly
with no name
function () { echo 'Hello world!'; };
- 50. A lambda can be stored
in a variable
$hello = function () { echo 'Hello world!'; };
- 51. And then it can be used
as any other PHP callable
$hello();
call_user_func($hello);
- 52. You can also pass a lambda
as an argument to a function or method
function foo(Closure $func) {
$func();
}
foo($hello);
- 53. $hello = function ($name) { echo 'Hello '.$name; };
$hello('Fabien');
call_user_func($hello, 'Fabien');
function foo(Closure $func, $name) {
$func($name);
}
foo($hello, 'Fabien');
- 55. 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; the closure to create
Ask
} t and pass the
the objec
public function getService($key) current Container
{
return $this->objects[$key]($this);
}
}
- 56. Description
$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);
});
Creating the User
$user = $container->getService('user'); is now as easy as before
- 57. Order does not
$container = new Container(); matter
$container->setService('storage', function ($c) {
return new $c->storage_class($c->session_name);
});
$container->setService('user', function ($c) {
return new User($c->getService('storage'));
});
$container->session_name = 'SESSION_ID';
$container->storage_class = 'SessionStorage';
- 58. class Container Simplify the code
{
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];
}
}
}
- 59. Unified interface
$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;
- 61. For some objects, like the user,
the container must always
return the same instance
- 66. A closure is a lambda
that remembers the context
of its creation…
- 67. 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'),
);
- 68. $mapper = function ($article) {
return $article->getTitle();
};
$titles = array_map($mapper, $articles);
- 69. $method = 'getTitle';
$mapper = function ($article) use($method) {
return $article->$method();
};
$method = 'getAuthor';
$titles = array_map($mapper, $articles);
- 70. $mapper = function ($method) {
return function ($article) use($method) {
return $article->$method();
};
};
- 74. 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];
}
}
}
- 75. 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)) { 40 LO C for a fully-
}
$object = $callable($c);
feature d container
return $object;
};
}
}
- 76. $container = new Container();
$container->session_name = 'SESSION_ID';
$container->storage_class = 'SessionStorage';
$container->user = $container->asShared(function ($c) {
return new User($c->storage);
});
$container->storage = $container->asShared(function ($c) {
return new $c->storage_class($c->session_name);
});
- 79. $container = new Container();
$container->session_name = 'SESSION_ID';
$container->storage_class = 'SessionStorage';
$container->user = $container->asShared(function () {
return new User($this->storage);
});
$container->storage = $container->asShared(function () {
return new $this->storage_class($this->session_name);
});
- 81. class Article
{
public $title;
public function __construct($title)
{
$this->title = $title;
}
}
$a = new Article('Title 1');
$title = function () {
echo $this->title;
};
$getTitle = $title->bindTo($a);
$getTitle();
- 82. class Article
{
private $title;
public function __construct($title)
{
$this->title = $title;
}
}
$a = new Article('Title 1');
$title = function () {
echo $this->title;
};
$getTitle = $title->bindTo($a, $a);
$getTitle();
- 85. function asShared($callable)
{
$callable = $callable->bindTo($this);
return function () use ($callable) {
static $object;
if (is_null($object)) {
$object = $callable();
}
return $object;
};
}
- 86. $container = new Container();
$container->session_name = 'SESSION_ID';
$container->storage_class = 'SessionStorage';
$container->user = $container->asShared(function () {
return new User($this->storage);
});
$container->storage = $container->asShared(function () {
return new $this->storage_class($this->session_name);
});
- 89. Good rule of thumb:
It manages “Global” objects
Objects with only one instance (!= Singletons)
- 90. LIKE
a User, a Request,
a database Connection, a Logger, …
- 96. namespace SymfonyComponentDependencyInjection;
interface ContainerInterface
{
function set($id, $service, $scope = self::SCOPE_CONTAINER);
function get($id, $invalidBehavior =
self::EXCEPTION_ON_INVALID_REFERENCE);
function has($id);
function getParameter($name);
function hasParameter($name);
function setParameter($name, $value);
function enterScope($name);
function leaveScope($name);
function addScope(ScopeInterface $scope);
function hasScope($name);
function isScopeActive($name);
}
- 99. parameters:
session_name: SESSION_ID
storage_class: SessionStorage
services:
user:
class: User
arguments: [@storage]
storage:
class: %storage_class%
arguments: [%session_name%]
- 101. <container xmlns="http://symfony.com/schema/dic/services">
<parameters>
<parameter key="session_name">SESSION_ID</parameter>
<parameter key="storage_class">SessionStorage</parameter>
</parameters>
<services>
<service id="user" class="User">
<argument type="service" id="storage" />
</service>
<service id="storage" class="%storage_class%">
<argument>%session_name%</argument>
</service>
</services>
</container>
- 102. <container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="session_name">SESSION_ID</parameter>
<parameter key="storage_class">SessionStorage</parameter>
</parameters>
<services>
<service id="user" class="User">
<argument type="service" id="storage" />
</service>
<service id="storage" class="%storage_class%">
<argument>%session_name%</argument>
</service>
</services>
</container>
- 104. As fast as it can be
The container can be
“compiled” down
to plain PHP code
- 106. class ProjectServiceContainer extends Container
{
public function __construct()
{
parent::__construct(new ParameterBag($this->getDefaultParameters()));
}
protected function getStorageService()
{
$class = $this->getParameter('storage_class');
return $this->services['storage'] = new $class($this-
>getParameter('session_name'));
}
protected function getUserService()
{
return $this->services['user'] = new User($this->get('storage'));
}
protected function getDefaultParameters()
{
return array(
'session_name' => 'SESSION_ID',
'storage_class' => 'SessionStorage',
);
}
}
- 107. $dumper = new GraphvizDumper($sc);
echo $dumper->dump();
$ doc -Tpng -o sc.png sc.dot
- 108. There is more...
Semantic configuration
Scope management
Compiler passes (optimizers)
Aliases
Abstract definitions
...
- 109. Remember, most of the time,
you don’t need a Container
to use Dependency Injection
- 110. You can start to use and benefit from
Dependency Injection today
- 111. by implementing it
in your projects
by using externals libraries
that already use DI
without the need of a container
- 114. 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://sensiolabs.com/
http://symfony.com/
http://fabien.potencier.org/