Friday, June 17, 2011

Symfony Dependency Injection - a really cool PHP tool

Hi!

Today I want to talk a little about a great design pattern called Dependency Injection and it's implementation by SensioLabs called simply Dependency Injection Component.

Now dependency injection is basically the inversion of control in you classes. It can help you to create decoupled structures that can be configured at run time which gives you a lot of flexibility. Let's take a look at an example:

Let's presume, you need to user a tool class inside some other class:

class User {


private $tool;


public function __construct(Tool $tool){
   $this->tool = $tool;
}


public function doStuff()   
   $this->tool->doSomeStuff();
}


As you can see, I've user the strategy pattern, so every time I want the USer to ->doStuff() it uses the ->doSomeStuff() method of the Tool class object.

Now this way is pretty neat, because it avoids  hard-coding the Tool into the User allowing for polymorphism. However creating a lot of such dependencies can be a pain if you have some more complicated stuff to do. Additionally there can be some problems with instantiating the Tool class before usage.

Symfony dependency injection can solve both those problems as it decouples elements and creates them upon usage, not upon configuration and helps to easily create complicated dependencies just before usage. Let's look a the example ripped out of my ZF project:


//retrieving mailer options from configuration

$mailerOptions = $this->getOption('mailer');


//we create a builder object, that will later build our classes
$serviceContainerBuilder = new sfServiceContainerBuilder();


//regitering the mailer service
//this means, that when we call the mailer service
//the Zend_Mail class has to be instantiated with parameters
//and a function called with some additional params


$serviceContainerBuilder->register('mailer', 'Zend_Mail')
->addMethodCall('setDefaultFrom', array($mailerOptions['from'], $mailerOptions['from_name']))
->addMethodCall('setDefaultReplyTo', array($mailerOptions['replyto'], $mailerOptions['replyto_name']))
->addMethodCall('setDefaultTransport', array('%mailer.transport%'));


//everything should be pretty straight forward
//I'm creating a builder instance and configuring it
//with the object class I wan't to have created
//and it's parameters
//the setDefaultTransport param is still open, when you add
//params like %param% sfDI knows that you want to set the param
//before service class instantiation


//setting to Zend_Registry to get the serviceContainer whenever I want
Zend_Registry::set('serviceContainer', $serviceContainerBuilder);



Some of you, who have some experience with dealing with Zend_Mail, know that you need to have a mailer transport of type Zend_Mail_Transport_Abstract.

In my application I have two bootstrapping (preparing basically) files:
1) the main application bootstrap file (from which it was I took the above example)
2) the PHPUnit bootstrap

The testing reasons I want to have a different transport in the second bootstrap that in the reall one. The first mailer should normally send the email but the second one should just log it in a file. Here the dependency injection copes in really handy:

1) Main bootstrap:



$serviceContainerBuilder = Zend_Registry::get('serviceContainer');
$mailerOptions = $this->getOption('mailer');


//I want the main transport to be a posrmarkapp.com using transport
$serviceContainerBuilder->register('mail.transport', 'FT_Mail_Transport_Postmark')
->addArgument($mailerOptions['transport']['api_key'])
->setShared(false);


//the setShared(false) simply tells the container that the mail.transport object isn't unique


//here is the interesting part
//I tell the Container to inject the newly configured container, as the 
//"mailer.transport" argument to the mail.transport container


$serviceContainerBuilder->setParameter('mailer.transport', new sfServiceReference('mail.transport'));



2) The second bootstrap (PHPUnit):



$serviceContainerBuilder = Zend_Registry::get('serviceContainer');
$serviceContainerBuilder->register('mail.transport', 'Zend_Mail_Transport_File')
->addArgument(array('path' => APPLICATION_PATH . '/..tests/log'))
->setShared(false);



This example shows just how you can create another transport for another purpose, just as easily.
Now we want to see it all in action:


//in some form processing command, we retrieve the container and create the service

$mailerService = \Zend_Registry::get('serviceContainer')->getService('mailer');


$tpl = //obviously we need some template


$mailerService->setBodyHtml($tpl->render());
$mailerService->addTo($emailTo);
$mailerService->send();



It's that simple, we can additionally set some parameters before getting the service, adding different behavior to the container.
Some of you might say that you can preconfigure the Zend_Mail class instance and set if directly into the registry. True, but remember that not all pages result in the usage of the mailer, so you would probably waste a lot of resources holding a lot if instantiated objects in the registry.

Hope you enjoyed the examples and start building your complicated class relations with the Symfony Dependency Injection Component. For more information go to http://components.symfony-project.org/dependency-injection/

cheers,
Peter

5 comments:

Kuba said...

Hi, this is an old, not maintained component. Latest version is part of Symfony2 git repository: https://github.com/symfony/symfony/tree/master/src/Symfony/Component/DependencyInjection

It's still a component and as all of them can be used without Symfony.

pejot said...

Many thanks for noticing, will have to update my code :)

How is the compatibility?

Kuba said...

I don't think it's compatible as it evolved a lot. Also, new component is PHP 5.3 only.

venba said...

sises

venba said...




Thank you for the info. It sounds pretty user friendly. I guess I’ll pick one up for fun. thank u












Form Processing Services