Software Alchemist

I am constantly seeking answers and ways of transforming desires into reality by coding

Symfony2 DIC Component Overview

| Comments

As some of you might know, the Symfony2 framework consists of two main ingredients:

  • Components
  • Bundles

The logical separation should be the following:

The Symfony Components are standalone and reusable PHP classes. With no pre-requisite, except for PHP, you can install them today, and start using them right away. Symfony Components Web Site

A Bundle is a structured set of files (PHP files, stylesheets, JavaScripts, images, etc.) that implements a single feature (a blog, a forum, …) and which can be easily shared with other developers. Symfony2 Documentation

Of course, there are different vendor libraries that Symfony2 uses, that are not Components or Bundles. Its important to remember, that in order to expose that functionality in your Symfony2 application and make it accessible, you have to create a Bundle. Its a good practice and an unwritten convention.

I think that the main reason for doing so is to avoid setting up third party libraries yourself and delegate that to Symfony2’s DIC component, which was built for that very purpose. This lets other developers overload some of your configuration, class names and parameters without modifying your core classes and breaking backwards compatibility.

DIC stands for Dependency Injection Container.

The main idea behind Dependency Injection Containers is to extract all the instantiation and wiring logic from your application into a well-tested dedicated component, avoiding the code duplication that inevitably happens if you’re practicing Dependency Injection and Testability without DIC. By removing all of the setup code, Symfony2 removes another possibility of error and lets you concentrate on your domain problems instead of object instantiation.

Each object in Symfony2 DIC is called a Service. Service is an instance of some Class, that is created either by direct instantiation using the new operator or using some other Service’s factory method, that gets certain dependencies injected into it as part of the instantiation process.

It is much easier to understand how services are configured by looking at an example configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">
    <parameters>
        <parameter key="payment_gateway.adapter.paypal.username">API_USERNAME</parameter>
        <parameter key="payment_gateway.adapter.paypal.token">API_TOKEN</parameter>
        <parameter key="payment_gateway.adapter.authorize_net.config" type="collection">
            <parameter key="username">API_USERNAME</parameter>
            <parameter key="token">API_TOKEN</parameter>
            <parameter key="version">V2</parameter>
        </parameter>
    </parameters>
    <services>
        <service id="payment_gateway.adapter.paypal" class="MyCompany\Component\Payment\Gateway\Adapter\Paypal">
            <argument>%payment_gateway.adapter.paypal.username%</argument>
            <argument>%payment_gateway.adapter.paypal.token%</argument>
        </service>
        <service id="payment_gateway.adapter.authorize_net" class="MyCompany\Component\Payment\Gateway\Adapter\AuthorizeNet">
            <argument>%payment_gateway.adapter.authorize_net.config%</argument>
        </service>
        <service id="payment_gateway" class="MyCompany\Component\Payment\Gateway">
            <call method="setAdapter">
                <argument>paypal</argument>
                <argument type="service" id="payment_gateway.adapter.paypal" />
            </call>
            <call method="setAdapter">
                <argument>authorize_net</argument>
                <argument type="service" id="payment_gateway.adapter.authorize_net" />
            </call>
        </service>
    </services>
</container>

I personally find it very readable.

During the container instantiation, the XmlFileLoader takes the above-mentioned services.xml file and transforms it into PHP code, which looks similar to the following pseudo-code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

use Symfony\Component\DependencyInjection\Container;

$container = new Container();

$container->setParameter('payment_gateway.adapter.paypal.username', 'API_USERNAME');
$container->setParameter('payment_gateway.adapter.paypal.token', 'API_TOKEN');
$container->setParameter('payment_gateway.adapter.authorize_net.config', array(
    'username' => 'API_USERNAME',
    'token'    => 'API_TOKEN',
    'version'  => 'V2',
));

$paypal = new \MyCompany\Component\Payment\Gateway\Adapter\Paypal(
    $container->getParameter('payment_gateway.adapter.paypal.username'),
    $container->getParameter('payment_gateway.adapter.paypal.token')
);
$container->setService('payment_gateway.adapter.paypal', $paypal);

$authorizeNet = new \MyCompany\Component\Payment\Gateway\Adapter\AuthorizeNet(
    $container->getParameter('payment_gateway.adapter.authorize_net.config')
);
$container->setService('payment_gateway.adapter.authorize_net', $authorizeNet);

$gateway = new \MyCompany\Component\Payment\Gateway();
$gateway->setAdapter('paypal', $container->getService('payment_gateway.adapter.paypal'));
$gateway->setAdapter('authorize_net', $container->getService('payment_gateway.adapter.authorize_net'));
$container->setService('payment_gateway', $gateway);

Now you have sort of a bird-eye view of how your objects are built and interact all in one place. No need to open some bootstrap file to see how everything gets wired together, and most importantly, no need to touch your code in order to change how things get wired together. Ideally, we want application to be able to perform completely different tasks, just by re-arranging some dependencies.

note All of your DI xml (or yaml or php) configurations need to live under <bundle name>/Resources/config directory of your application, in our example, I would store the configuration in MyCompany/PaymentBundle/Resources/config/services.xml.

The next step is to let your Symfony2 application know that you have this service configuration and want it to be included in the main application container. The way you do it is very conventional, although I know at least one way to make it configurable, but that’s a different topic and deserves its own blog post.

In order to include your custom configuration, you usually need to create something called Dependency Injection Extension. A DI Extension is a class, that lives under <bundle name>/DependencyInjection directory, that implements Symfony\Component\DependencyInjection\Extension\ExtensionInterface and which name is suffixed with Extension.

Inside that class, you need to implement four methods:

  • public function load($tag, array $config, ContainerBuilder $configuration);
  • public function getNamespace();
  • public function getXsdValidationBasePath();
  • public function getAlias();

Or you could choose to extend Symfony\Component\DependencyInjection\Extension\Extension and have to worry only about the last three.

Let’s look at an example extension, that would register our services.xml configuration file with Symfony2’s DIC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php

namespace MyCompany\Bundle\PaymentBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class PaymentExtension extends Extension
{
    /**
     * Loads the services based on your application configuration.
     * The full configuration is as follows:
     *
     * payment.config:
     *   paypal:
     *     username: email@domain.com
     *     token:    XXXXX-XXXXX-XXX-X
     *   authorize_net:
     *     config:
     *       username: email@domain.com
     *       token:    XXXXXX-XXXXX-XXX-X
     *       version:  V2
     *
     * @param mixed $config
     */
    public function configLoad($config, ContainerBuilder $container)
    {
        if (!$container->hasDefinition('payment_gateway')) {
            $loader = new XmlFileLoader($container, __DIR__.'/../Resources/config');
            $loader->load('services.xml');
        }
        if (isset($config['paypal'])) {
            foreach (array('username', 'token') as $key) {
                if (isset($config['paypal'][$key]) {
                    $container->setParameter('payment_gateway.adapter.paypal.'.$key, $config['paypal'][$key]);
                }
            }
        }
        if (isset($config['authorize_net']['config'])) {
            $parameters = $container->getParameter('payment_gateway.adapter.authorize_net.config');
            foreach (array('username', 'token', 'version') as $key) {
                if (isset($config['authorize_net']['config'][$key])) {
                    $parameters[$key] = $config['authorize_net']['config'][$key];
                }
            }
            $container->setParameter('payment_gateway.adapter.authorize_net.config', $parameters);
        }
    }

    /**
     * @inheritDoc
     */
    public function getXsdValidationBasePath()
    {
        return __DIR__.'/../Resources/config/schema';
    }

    /**
     * @inheritDoc
     */
    public function getNamespace()
    {
        return 'http://avalanche123.com/schema/dic/payment';
    }

    /**
     * @inheritDoc
     */
    public function getAlias()
    {
        return 'payment';
    }
}

This extension does several things:

  • It will include the services.xml into DIC only if payment_gateway service is not yet defined - this is to avoid conflicts and lazy-load the configuration.
  • It will override some of default parameters, if you specify your own when enabling the extension.
  • It also provides the XSD schema location and base path for validation of XML configuration.

After you created the extension, all you need to do is add PaymentBundle to the application Kernel::registerBundles() method’s returned array. Then in the application configuration file specif something like payment.config: ~& (assuming you’re using yaml configs). That should do it, you should now be able to call $container->getService('payment_gateway') and get the fully set up instance of Gateway.

Happy Coding!

Comments