dependency injection

101
Dependency Injection with PHP 5.3 Fabien Potencier

Upload: fabien-potencier

Post on 15-Jan-2015

2.034 views

Category:

Business


1 download

DESCRIPTION

 

TRANSCRIPT

Page 1: Dependency Injection

Dependency Injection with PHP 5.3

Fabien Potencier

Page 2: Dependency Injection

Fabien Potencier

Serial entrepreneur Developer by passion Founder of Sensio Creator and lead developer of Symfony On Twitter @fabpot On github http://www.github.com/fabpot Blog http://fabien.potencier.org/

Page 3: Dependency Injection

Dependency Injection

A real world « web » example

Page 4: Dependency Injection

In most web applications, you need to manage the user preferences

–  The user language – Whether the user is authenticated or not –  The user credentials – …

Page 5: Dependency Injection

This can be done with a User object

–  setLanguage(), getLanguage() –  setAuthenticated(), isAuthenticated() –  addCredential(), hasCredential() –  ...

Page 6: Dependency Injection

The User information need to be persisted

between HTTP requests

We use the PHP session for the Storage

Page 7: Dependency Injection

class SessionStorage { function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); }

function set($key, $value) { $_SESSION[$key] = $value; }

// ... }

Page 8: Dependency Injection

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

Page 9: Dependency Injection

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

Page 10: Dependency Injection

That’s Dependency Injection

Nothing more

Page 11: Dependency Injection

Let’s understand why the first example is not a good idea

Page 12: Dependency Injection

I want to change the session cookie name

Page 13: Dependency Injection

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

Page 14: Dependency Injection

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?

Page 15: Dependency Injection

class User { protected $storage;

function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } }

$user = new User('SESSION_ID');

Configure via

User?

Page 16: Dependency Injection

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?

Page 17: Dependency Injection

I want to change the session storage implementation

Filesystem MySQL

Memcached …

Page 18: Dependency Injection

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?

Page 19: Dependency Injection

Now, the User depends on the Registry

Page 20: Dependency Injection

Instead of harcoding the Storage dependency

inside the User class constructor

Inject the Storage dependency in the User object

Page 21: Dependency Injection

class User { protected $storage;

function __construct($storage) { $this->storage = $storage; } }

$storage = new SessionStorage('SESSION_ID'); $user = new User($storage);

Page 22: Dependency Injection

What are the advantages?

Page 23: Dependency Injection

Use different Storage strategies

Page 24: Dependency Injection

class User { protected $storage;

function __construct($storage) { $this->storage = $storage; } }

$storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);

Use a different

Storage engine

Page 25: Dependency Injection

Configuration becomes natural

Page 26: Dependency Injection

class User { protected $storage;

function __construct($storage) { $this->storage = $storage; } }

$storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);

Configuration

is natural

Page 27: Dependency Injection

Wrap third-party classes (Interface / Adapter)

Page 28: Dependency Injection

class User { protected $storage;

function __construct(SessionStorageInterface $storage) { $this->storage = $storage; } }

interface SessionStorageInterface { function get($key);

function set($key, $value); }

Add an interface

Page 29: Dependency Injection

Mock the Storage object (for testing)

Page 30: Dependency Injection

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

Page 31: Dependency Injection

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

Page 32: Dependency Injection

« Dependency Injection is where components are given their dependencies

through their constructors, methods, or directly into fields. »

http://www.picocontainer.org/injection.html

Page 33: Dependency Injection

$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;

Page 34: Dependency Injection

A slightly more complex web example

Page 35: Dependency Injection

$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);

Page 36: Dependency Injection

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();

Page 37: Dependency Injection

Back to square 1

Page 38: Dependency Injection

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();

Page 39: Dependency Injection

We need a Container

Describes objects and their dependencies

Instantiates and configures objects on-demand

Page 40: Dependency Injection

A container SHOULD be able to manage

ANY PHP object (POPO)

The objects MUST not know that they are managed

by a container

Page 41: Dependency Injection

•  Parameters –  The SessionStorageInterface implementation we want to use (the class name) –  The session name

•  Objects –  SessionStorage –  User

•  Dependencies –  User depends on a SessionStorageInterface implementation

Page 42: Dependency Injection

Let’s build a simple container with PHP 5.3

Page 43: Dependency Injection

DI Container

Managing parameters

Page 44: Dependency Injection

class Container { protected $parameters = array();

public function setParameter($key, $value) { $this->parameters[$key] = $value; }

public function getParameter($key) { return $this->parameters[$key]; } }

Page 45: Dependency Injection

$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

Page 46: Dependency Injection

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

Page 47: Dependency Injection

$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

Page 48: Dependency Injection

DI Container

Managing objects

Page 49: Dependency Injection

We need a way to describe how to create objects, without actually instantiating anything!

Anonymous functions to the rescue!

Page 50: Dependency Injection

Anonymous Functions / Lambdas

A lambda is a function defined on the fly

with no name

function () { echo 'Hello world!'; };

Page 51: Dependency Injection

Anonymous Functions / Lambdas

A lambda can be stored in a variable

$hello = function () { echo 'Hello world!'; };

Page 52: Dependency Injection

Anonymous Functions / Lambdas

And then it can be used as any other PHP callable

$hello();

call_user_func($hello);

Page 53: Dependency Injection

Anonymous Functions / Lambdas

You can also pass a lambda as an argument to a function or method

function foo(Closure $func) { $func(); }

foo($hello);

Page 54: Dependency Injection

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');

Page 55: Dependency Injection

DI Container

Managing objects

Page 56: Dependency Injection

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

Page 57: Dependency Injection

$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

Page 58: Dependency Injection

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

Page 59: Dependency Injection

$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

Page 60: Dependency Injection

DI Container

Scope

Page 61: Dependency Injection

For some objects, like the user, the container must always return the same instance

Page 62: Dependency Injection

spl_object_hash($container->user)

!== spl_object_hash($container->user)

Page 63: Dependency Injection

$container->user = function ($c) { static $user;

if (is_null($user)) { $user = new User($c->storage); }

return $user; };

Page 64: Dependency Injection

spl_object_hash($container->user)

=== spl_object_hash($container->user)

Page 65: Dependency Injection

$container->user = $container->asShared(function ($c) { return new User($c->storage); });

Page 66: Dependency Injection

A closure is a lambda that remembers the context

of its creation…

Page 67: Dependency Injection

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'), );

Page 68: Dependency Injection

$mapper = function ($article) { return $article->getTitle(); };

$titles = array_map($mapper, $articles);

Page 69: Dependency Injection

$method = 'getTitle';

$mapper = function ($article) use($method) { return $article->$method(); };

$method = 'getAuthor';

$titles = array_map($mapper, $articles);

Page 70: Dependency Injection

$mapper = function ($method) { return function ($article) use($method) { return $article->$method(); }; };

Page 71: Dependency Injection

$titles = array_map($mapper('getTitle'), $articles);

$authors = array_map($mapper('getAuthor'), $articles);

Page 72: Dependency Injection

$container->user = $container->asShared(function ($c) { return new User($c->storage); });

Page 73: Dependency Injection

function asShared(Closure $lambda) { return function ($container) use ($lambda) { static $object;

if (is_null($object)) { $object = $lambda($container); } return $object; }; }

Page 74: Dependency Injection

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

Page 75: Dependency Injection

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

Page 76: Dependency Injection

I’m NOT advocating the usage of lambdas everywhere

This presentation is about showing how they work

on practical examples

Page 77: Dependency Injection

A DI Container does NOT manage ALL your objects

Page 78: Dependency Injection

Good rule of thumb: It manages “Global” objects

Objects with only one instance (!= Singletons)

Page 79: Dependency Injection

LIKE a User, a Request,

a database Connection, a Logger, …

Page 80: Dependency Injection

UNLIKE Model objects (a Product, a blog Post, …)

Page 81: Dependency Injection

Symfony Components Dependency Injection

Page 82: Dependency Injection

Rock-solid implementation of a DIC in PHP 5.3

Page 83: Dependency Injection

At the core of the Symfony 2.0 framework

… which is one of the fastest framework

Page 84: Dependency Injection

Very flexible

Configuration in PHP, XML, YAML, or INI

Page 85: Dependency Injection

$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 xmlns="http://symfony-project.org/schema/dic/services"> <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>

PHP

YAML

XML

Page 86: Dependency Injection

<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 }

Page 87: Dependency Injection

Fast as hell

The container can be “compiled” down to plain PHP code

Page 88: Dependency Injection

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->getOutputService(), array('with_newline' => true));

return $this->shared['message'] = $instance; } }

Page 89: Dependency Injection

Semantic configuration

Thanks to an extension mechanism

Page 90: Dependency Injection

<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>

Page 91: Dependency Injection

<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" />

<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>

auto-completion

and validation with XSD

Page 92: Dependency Injection

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

Page 93: Dependency Injection

Everything is converted by the extension to plain services and parameters

no overhead

Page 94: Dependency Injection

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();

Page 95: Dependency Injection

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/

Page 96: Dependency Injection

Remember, most of the time, you don’t need a Container

to use Dependency Injection

Page 97: Dependency Injection

You can start to use and benefit from Dependency Injection today

Page 98: Dependency Injection

by implementing it in your projects

by using externals libraries that already use DI

without the need of a container

Page 99: Dependency Injection

Symfony Zend Framework ezComponents

Doctrine Swift Mailer

Page 100: Dependency Injection

Questions?

My slides will be available on slideshare.com/fabpot

Page 101: Dependency Injection

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/