You are on page 1of 51

Dependency Injection and the Symfony2 Service Container

Richard Miller

http://www.limethinking.co.uk http://miller.limethinking.co.uk @mr_r_miller

What is Dependency Injection?

class {

ProductController

public function addAction() { //validate the POST variables $productSaver = new ProductSaver(); $productSaver->save($product); } //-}

class ProductSaver { public function save($product) { $mapper = new DataMapper(); $mapper->save($product); $emailNotifier = new EmailNotifier(); $emailNotifier->notify($product); } }

class EmailNotifier { public function notify($product) { $mail = new Mail(); $mail->addTo('productmanager@example.com'); $mail->addCC('deputyproductmanager@example.com'); //create message body using $product $mailer = new Mailer(); $mailer->send($mail); } }

Problems with EmailNotifier

Not reusable

Change of email addresses Change of mailer Any test will create an actual Mailer and send the email

Impossible to unit test

protected $toAddress; public function __construct($toAddress) { $this->toAddress = $toAddress; } public function notify($product) { $mail = new Mail(); $mail->addTo($this->toAddress); $mail->addCC('deputyproductmanager@example.com'); //create message body using $product $mailer = new Mailer(); $mailer->send($mail); }

protected $toAddress; array(); protected $ccAddresses = array (); public function __construct($toAddress) { $this->toAddress = $toAddress; } public function addCcAddress($ccAddress) setCcAddress($ccAddress) { $this->ccAddresses[] = $ccAdresss; } public function notify($product) { $mail = new Mail(); $mail->addTo($this->toAddress); foreach($this->ccAddresses foreach ($this->ccAddresses as $ccAddress) { $mail->addCC($ccAddress); } //create message body using $product $mailer = new Mailer(); $mailer->send($mail); }

Constructor Injection

Fixed at instantiation Guaranteed to have been injected

Setter Injection

Good for optional dependencies Good for collections of dependencies

protected $toAddress; protected $ccAddresses = array(); protected $mailer; public function __construct($toAddress, MailerInterface MailerInterface $mailer $mailer) { $this->toAddress = $toAddress; $this->mailer = $mailer; } public function addCcAddress($ccAddress) { $this->ccAddresses[] = $ccAdresss; } public function notify($product) { $mail = new Mail(); $mail->addTo($this->toAddress); foreach($this->ccAddresses as $ccAddress) { $mail->addCC($ccAddress); } //create message body using $product $this->mailer->send($mail); }

$settings = Settings::fetch(); switch($settings->get('mailer)) { case 'smtp': $mailer = new SMTPMailer(); break; case 'sendmail': $mailer = new SendmailMailer(); break; case 'test': $mailer = new TestMailer(); break; case 'basic': default: $mailer = new Mailer(); break; } $mailer->send($mail);

protected $toAddress; protected $ccAddresses = array(); protected $mailer; public function __construct($toAddress, MailerInterface MailerInterface $mailer $mailer) { $this->toAddress = $toAddress; $this->mailer = $mailer; } public function addCcAddress($ccAddress) { $this->ccAddresses[] = $ccAdresss; } public function notify($product) { $mail = new Mail(); $mail->addTo($this->toAddress); foreach($this->ccAddresses as $ccAddress) { $mail->addCC($ccAddress); } //create message body using $product $this->mailer->send($mail); }

Type Hinting

Ensures object with correct interface injected Hint should be Interface not Implementation Helps IDE with auto-completion

protected $toAddress; protected $ccAddresses = array(); protected $mailer; public function __construct($toAddress, MailerInterface $mailer) { $this->toAddress = $toAddress; $this->mailer = $mailer; } public function addCcAddress($ccAddress) { $this->ccAddresses[] = $ccAdresss; } public function notify($product) { $mail = new Mail(); $mail->addTo($this->toAddress); foreach($this->ccAddresses as $ccAddress) { $mail->addCC($ccAddress); } //create message body using $product $this->mailer->send($mail); }

protected $toAddress; protected $ccAddresses = array(); protected $mailer; public function __construct($toAddress, MailerInterface $mailer) { $this->toAddress = $toAddress; $this->mailer = $mailer; } public function addCcAddress($ccAddress) { $this->ccAddresses[] = $ccAdresss; } public function notify($product) { $mail = = MailFactory::get(); MailFactory::get(); $mail $mail->addTo($this->toAddress); foreach($this->ccAddresses as $ccAddress) { $mail->addCC($ccAddress); } //create message body using $product $this->mailer->send($mail); }

//-protected $mailFactory; public function __construct($toAddress, MailerInterface $mailer, MailFactoryInterface $mailFactory ) { $this->toAddress = $toAddress; $this->mailer = $mailer; $this->mailFactory = $mailFactory; } //-public function notify($product) { $mail = $this->mailFactory->get(); $mail->addTo($this->toAddress); foreach($this->ccAddresses as $ccAddress) { $mail->addCC($ccAddress); } //create message body using $product $this->mailer->send($mail); }

Where does the creation of dependencies go ?

class ProductSaver { public function save($product) { $mapper = new DataMapper(); $mapper->save($product); $emailNotifier = new EmailNotifier(); $emailNotifier->notify($product); } }

public function save($product) { $mapper = new DataMapper(); $mapper->save($product); $emailNotifier = new EmailNotifier('productmanager@example.com', Mailer, new new Mailer, MailFactory new new MailFactory ); ); $emailNotifier->addCcAddress('deputyproductmanager@example.com'); $emailNotifier->setCcAddress('deputyproductmanager@example.com'); $emailNotifier->addCcAddress('salesteam@example.com'); $emailNotifier->setCcAddress('salesteam@example.com'); $emailNotifier->notify($product); }

protected $notifiers = array(); public function addNotifier(NotifierInterface $notifier) { $this->notifiers[] = $notifier; } public function save($product) { $mapper = new DataMapper(); $mapper->save($product); foreach($this->notifiers $notifier) foreach($this->notifiers as as $notifier) { { $notifier->notify($product); $notifier->notify($product); } }

protected $notifiers = array(); protected $mapper; $mapper; protected public function function __construct(DataMapperInterface __construct(DataMapperInterface $mapper) $mapper) public { { $this->mapper = = $mapper; $mapper; $this->mapper } } //-public function save($product) { $this->mapper->save($product); $this->mapper->save($product); foreach($this->notifiers as $notifier) { $notifier->notify($product); } }

Problem solved?

class {

ProductController

public function addAction() { //validate the POST variables $productSaver = new ProductSaver(); $productSaver->save($product); } //-}

public function addAction() { //validate the POST variables $emailNotifier = new EmailNotifier('productmanager@example.com', Mailer, new new Mailer, MailFactory new new MailFactory ); ); $emailNotifier->addCcAddress('deputyproductmanager@example.com'); $emailNotifier->setCcAddress('deputyproductmanager@example.com'); $emailNotifier->addCcAddress('salesteam@example.com'); $emailNotifier->setCcAddress('salesteam@example.com'); $mapper = new DataMapper(); $productSaver = new ProductSaver($mapper); ProductSaver($emailNotifier, $mapper); $productSaver->addNotifier($emailNotifier); $productSaver->setNotifier($emailNotifier); $productSaver->save($product); }

protected $productSaver; public function __construct($productSaver) { $this->productSaver = $productSaver; } public function addAction() { //validate the POST variables $this->productSaver->save($product); }

$emailNotifier = new EmailNotifier('productmanager@example.com', new Mailer, new MailFactory ); $emailNotifier->addCcAddress('deputyproductmanager@example.com'); $emailNotifier->addCcAddress('salesteam@example.com'); $mapper = new DataMapper(); $productSaver = new ProductSaver($mapper); $productSaver->addNotifier($emailNotifier); $controller = new ProductController($productSaver);

Advantages to separate wiring


Set up config differently for each app No config code in classes No need to maintain different versions for different apps Just inject object with different functionality

Disadvantages to manual wiring


Large unwieldy bootstrap file Everything set up whether used or not Repetitive code

The Symfony2 Service Container to the rescue


Creates and configures services Allows configuration with XML, YAML and PHP Only creates used services

<services> <service id="mailer" class="NameSpace\Of\Mailer"/> <service id="mailFactory" class="NameSpace\Of\MailFactory"/> </services>

<services> <service id="emailNotifer" class="NameSpace\Of\EmailNotifer"> <argument>productmanager@example.com</argument> <argument type="service" id="mailer" /> <argument type="service" id="mailFactory" /> </service> <service id="mailer" class="NameSpace\Of\Mailer"/> <service id="mailFactory" class="NameSpace\Of\MailFactory"/> </services>

<services> <service id="emailNotifer" class="NameSpace\Of\EmailNotifer"> <argument>productmanager@example.com</argument> <argument type="service" id="mailer" /> <argument type="service" id="mailFactory" /> method="setCcAddress"> <call method="addCcAddress"> <argument>deputyproductmanager@example.com</argument> <argument>deputyproductmanager@example.com</argument> </call> <call method="addCcAddress"> method="setCcAddress"> <argument>salesteam@example.com</argument> <argument>salesteam@example.com</argument> </call> </service> <service id="mailer" class="NameSpace\Of\Mailer"/> <service id="mailFactory" class="NameSpace\Of\MailFactory"/> </services>

services: emailNotifer: class: NameSpace\Of\EmailNotifer arguments: [productmanager@example.com, @mailer, @mailFactory] calls: - [ addCcAddress, [ deputyproductmanager@example.com ] ] - [ addCcAddress, [ salesteam@example.com ] ] mailer: class: NameSpace\Of\Mailer mailFactory: class: NameSpace\Of\MailFactory

$container->setDefinition('emailNotifer', new Definition( 'NameSpace\Of\EmailNotifer', array( 'productmanager@example.com', new Reference('mailer'), new Reference('mailFactory') ) ))->addMethodCall('addCcAddress', array( 'deputyproductmanager@example.com' ))->addMethodCall('addCcAddress', array( 'salesteam@example.com' )); $container->setDefinition('mailer', new Definition( 'NameSpace\Of\Mailer' )); $container->setDefinition('mailFactory', new Definition( 'NameSpace\Of\MailFactory' ));

How do we actually use the container?

$productController = $container->get('productController');

class ProductController extends Controller { public function addAction() { //validate the POST variables $productSaver = $this->container->get('productSaver'); $productSaver->save($product); } //-}

A Criticism of Dependency Injection

It's difficult to navigate code

Tools to help

container:debug command JMSDebuggingBundle

php app/console container:debug [container] Public services Service Id doctrine doctrine.dbal.default_connection doctrine.odm.mongodb.cache.array #-Class Name Symfony\Bundle\DoctrineBundle\Registry Doctrine\DBAL\Connection Doctrine\Common\Cache\ArrayCache

php app/console container:debug assetic.asset_manager [container] Information for service assetic.asset_manager Service Id Class Tags Scope Public assetic.asset_manager Assetic\Factory\LazyAssetManager container yes

JMSDebuggingBundle

http://github.com/schmittjoh/JMSDebuggingBun dle

Conclusion

Class level advantages


Promotes re-usability Allows true unit testing Allows easy configuration of application Easy to implement new features by dropping in new objects No need to maintain multiple code bases

App level advantages


More

Cookbook articles http://symfony.com/doc/current/cookbook/ http://miller.limethinking.co.uk @mr_r_miller http://joind.in/3704

You might also like