Quick Start Symfony DI (Dependency Injection) Tutorial

By , Saturday 14th August 2010 2:21 pm

What is Dependency Injection (DI)?

Dependency injection is a technique that allows for loosely coupled objects within a software application. Generally if an object requires access to the functionality of another it would be instantiated internally leading to tightly coupled systems. By implementing dependency injection we inject the required objects ready for use (sometimes also referred to inversion of control – IOC). Take the following example:

<?php
class DecisionMaker {
    public function makeDecision(array $parameters) {
        // Need the database adapter
        $dp = new DecisionParameters();
        $parameterScore = $dp->getScore($parameters);
        /* ... Some more decision logic ... */
        return ($parameterScore > 50);
    }
}

This piece of code is said to be tightly coupled to the DecisionParameters object. Rewriting the above in a loosely coupled fashion we’d have something like….

<?php
class DecisionMaker {
    private $_dp;
    public function __construct($dp) {
        $this->_dp = $dp;
    }
    public function makeDecision(array $parameters) {
        $parameterScore = $this->_dp->getScore($parameters);
        /* ... Some more decision logic ... */
        return ($parameterScore > 50);
    }
}

Whilst gaining the benefits of loosely coupled code we are adding complexity such that each time an object is instantiated we also have to instantiate its dependencies and pass these in too. For example, this:

$choice = new DecisionMaker();
echo $choice->makeDecision(array('effort' => 'low', 'return' => 'high'));

now becomes:

$dp = new DecisionParameters();
$choice = new DecisionMaker($dp);
echo $choice->makeDecision(array('effort' => 'low', 'return' => 'high'));

This situation becomes more painful as the number of dependencies for a class is increased, and what if the dependencies themselves have dependencies? This can quite quickly become an object administration nightmare! Enter dependency injection containers (or frameworks)…

Dependency Injection Containers/Frameworks

Dependency injection containers (or frameworks) handle the process of object creation; instantiating and injecting any dependencies before returning an instance to the caller.

In your code rather than create new objects directly we request a copy of the object from the DI container. The object we are returned already has  all its dependencies injected and the object is ready to go.

Symfony Dependency Injection Container

Symfony is probably best known for their full stack MVC framework however they have also released a number of the components that can be used independently. For example, the dependency injection container we’re going to talk about here, a YAML parser, a templating engine, see Symfony components for more.

The Symfony DI container is based on that from the Spring Framework in Java.

Bootstrapping

In order to bootstrap the Symfony dependency injection framework we use code as included below. I have chosen to use YAML just because its easy to read and setup. For maximum speed you may want to write out your containers to plain PHP (the Symfony container can do this for you once setup), or alternatively cache the whole container using Zend_Cache, or similar.

To install Symfony DI follow the instructions included here http://components.symfony-project.org/dependency-injection/installation, and add it to your path.

// Load the Symfony DI container
require 'sfServiceContainerBuilder.php';
$container = new sfServiceContainerBuilder();
$loader = new sfServiceContainerLoaderFileYaml($container);
$loader->load(APPLICATION_PATH . '/config/di/services.yml');

Firstly we instantiate a new container, and then we load our configuration from a YAML file. Note: the DI container can load config from several formats such as XML, YAML, PHP, and INI*.  I tend to include a single YAML file and import other files from within there.

Several configuration files can be imported using different formats, the newer definitions overwriting those that have already been defined. Configuration files can include references to objects and parameters.

*INI is only able to define parameters and is unable to import other files

Configuration Example

imports:
  - { resource: daos.yml }

parameters:
  username: false

services:
  # Customer model
  model.customer:
    class:  Pro_Customer
    calls:
      - [ setLogger, [ @utils.logger ] ]
      - [ setDao, [ @data.userdata.mysql ] ]
      - [ setUserName, [ %username% ] ]

  # Product model
  model.product:
    class:  Pro_Product
    arguments: [%username%, { type: %accesslevel%, lastlogin: %lastlogin% } ]
    calls:
     - [ setDao, [ @data.product.mysql ] ]

  # Logger
  utils.logger:
    class: Pro_Logger
    constructor: getInstance
    calls:
      - [ setHandle, [ @utils.filewriter ] ]

I think the code above is fairly self explanatory but for clarity I’ll explain each part now.

First off we define some imports (i.e. other files to parse), I like to group my configurations for example DAOs in one file, utilities in another and name the file appropriately. Whilst a little slower it speeds up the maintenance of the configuration files. Its also possible to parse files of other formats using different import flags. Files are parsed in order with newer definitions overwriting or amending previously defined services/parameters.

Next we define a parameter, a parameter can generally be any PHP variable type. At this point I didn’t know what my username parameter should be (I need to authenticate for that!), so I’ve defined a default value and I’ll overwrite that value later. Note, classes aren’t instantiated until you ask for them so defining parameters a little later is perfectly fine. Following this I define some services:

  1. Instantiate Pro_Customer, pass an instance of my logger to the setLogger() method, add in my MySQL data access object (DAO), and pass in the username as well. Each time I ask for this object I want a new instance
  2. Create an instance of Pro_Product, pass arguments of username and an options array to the constructor. After instantiation call setDao() and pass it my product DAO
  3. Give me a copy of Pro_Logger, instantiate it using the getInstance() method and pass a copy of my file writer object via setHandle() once its loaded. My file writer is defined in one of my imports.

Within the configuration files services are referenced by prepending the name with an ‘@’ symbol, parameters are referenced by prepending and appending with ‘%’ symbols, e.g. @utils.logger %username%.

Adding data post loading

Sometimes you don’t know what the value of parameters should be until after you have bootstrapped, as with our username parameter above. In order to overwrite the value the offsetSet() method is used, firstly passing the parameter name followed by its new value:

$container->offsetSet('username', $username);

Getting Objects from the container

Once setup rather than instantiating objects directly we now go to the DI container to get instances. With the Symfony DI we call the getService() method, passing in a string which describes the variable you wish to retrieve, e.g.

$logger = $container->getService('utils.logger');

This will perform the equivalent of:

$logger = Pro_Logger::getInstance();
$fileWriter = new Pro_Writer_FileWriter();
$logger->setHandler($fileWriter);

It is possible to determine if the container has an instance of a service by calling the hasService() method, which returns a boolean value.

Its not necessary to have access to the container beyond the top level of your application since once retrieving an instance all of the required dependencies right down to the deepest depths of your application are already setup and waiting to be called.

Unit Testing

Dependency injection also has the added benefit of allowing objects to be tested in isolation. Taking the DecisionMaker class example before implementing the dependency injection setup our test results were also dependent on the implementation of the DecisionParameters class.

Should the DecisionParameters class return different results our tests could start failing through no fault of our own. Using dependency injection we can now pass in a DecisionParameters object which returns a known/fixed set of results for certain input parameters, we are now testing DecisionMaker in isolation from any external factors, i.e. if our tests start failing this can be directly attributed to something changing in the DecisionMakeer class. This is particularly important should you be relying on test data from a database for testing.

Application Configuration

The way in which your application behaves on your production server will inevitably differ from your development setup. For example, in a development environment the level of logging would be much more granular than on your production system. By altering your dependency injection container configuration files slightly (or parsing an additional configuration file) the altering of behaviour depending on the environment is made simple.

By type hinting on interfaces rather than implementations when setting your dependencies its also possible to swap out components for compatible components with a few lines of YAML or XML. For instance, you may currently access your database via a MySQL DAO (Data Access Object), but in time you may develop a PDO, Zend_Db, or Doctrine implementation of this DAO. By adding the new implementation into your configuration file suddenly all the objects that used the old MySQL implementation are now using your new implementation without needing to attack the bowels of your application.

Notes

  • By setting shared: true in the configuration we are always given the same instance of an object. This is very useful when dealing with objects which contain resources such as database connections, file handles, etc
  • There are two methods by which dependencies can be injected. One is to pass dependencies with the constructor, or alternatively via setter methods. The generally accepted pattern is to pass required dependencies via the constructor and optionals via setters. My personal preference is to use setters for everything, but this is down to the individual developer

Finally….

Along with a brief discussion on dependency injection and its advantages and disadvantages this has also been a quick-start guide to implementing the Symfony Dependency Injection Container… hopefully you’ve seen that it’s surprisingly quick and easy. From here you’ll be able to start using dependency injection through your application and look to using many more of the advanced features (although the simple example covers the vast majority of functionality you’ll require). As always I point you to the manual for further information.

Liked this post? Follow this blog to get more. 

4 Responses to “Quick Start Symfony DI (Dependency Injection) Tutorial”

  1. […] This post was mentioned on Twitter by Vincent Jousse, Steven Lloyd Watkin. Steven Lloyd Watkin said: http://bit.ly/cUO2ov Quick start for #symfony dependency injection framework #php #zf […]

  2. Hari K T says:

    Great article.
    I have a doubt now . When bootstrapping we are loading the yaml configuration files . So does that create objects for all , which we don’t need ?
    For eg : I have a class Something which is used only sometimes. So does it creates an object or is it smart enough to load via autoload functions when I am calling getInstance() method . I didn’t get that , ie why .

    Thanks

  3. No, classes are only loaded when they are first required.

    In the getInstance() example the class is instantiated like:
    $class = Class::getInstance();
    Rather than:
    $class = new Class();
    Generally this is used when implementing the singleton pattern.

  4. […] letting you use DI in your project in an easy way. Steven Lloyd Watkin spend his time on writing a quick start tutorial.It's worth to mention that Symfony DI Container is a standalone library available as a Symfony […]

Leave a Reply

You must be logged in to post a comment.

Panorama Theme by Themocracy

2 visitors online now
1 guests, 1 bots, 0 members
Max visitors today: 3 at 12:13 am UTC
This month: 58 at 12-07-2016 03:48 pm UTC
This year: 58 at 12-07-2016 03:48 pm UTC
All time: 130 at 28-03-2011 10:40 pm UTC