Top Banner
Dependency Injection with PHP 5.3 Fabien Potencier
101

Dependency Injection

Jan 15, 2015

Download

Business

 
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
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/