Understanding Symfony Bundle Configuration and Service Container

In this post we’ll cover different ways on how to configure Bundles in Symfony2 and how the dependency injection container works with the configuration. The Bundle configuration and Symfony dependency injection container (also known as service container) can be difficult concepts to grasp when first starting development with Symfony2, especially if dependency injection is not a familiar concept beforehand. Bundle configuration can also be a little bit confusing, since there are multiple ways to do it and the best approach depends on the situation.

All of the configuration examples in this post are in YAML. Symfony also supports other configuration formats (XML and PHP array) and they are valid options. I am used to working with YAML because I think it’s more readable than XML, but you do get the benefit of schema validation when using XML. The choice of the configuration format is up to you (or your project team) and there is no right or wrong option here. Just use the one you feel most comfortable with.

Bundle creation

A Bundle is a directory containing a set of files (PHP files, stylesheets, JavaScripts, images, …) that implement a single feature (a blog, a forum, etc). In Symfony2, (almost) everything lives inside a bundle.

or, in other words, from the docs

A bundle is similar to a plugin in other software, but even better. The key difference is that everything is a bundle in Symfony2, including both the core framework functionality and the code written for your application. Bundles are first-class citizens in Symfony2. This gives you the flexibility to use pre-built features packaged in third-party bundles or to distribute your own bundles. It makes it easy to pick and choose which features to enable in your application and to optimize them the way you want.

When you create a new bundle, either by auto-generating it (php app/console generate:bundle --namespace=Acme/TestBundle) or manually, you need the BundleNameBundle.php file at the root directory of the bundle. The class in this file, while mostly empty, extends the Symfony core Symfony\Component\HttpKernel\Bundle\Bundle class and it is what you need to register in the AppKernel registerBundles method. When the kernel boots, it creates instances of each bundle and loads the container extension of each bundle (using methods in the parent Bundle class). The container extension is the class in BundleNameExtension.php file inside the DependencyInjection folder of the bundle. The container extension class loads and manages the bundle configuration. The whole extension class is optional and the bundle will work without it, but it’s good to know how things work to better understand the configuration system.

Loading the bundle configuration, the easy way

As I stated above, the extension class is optional and there are other ways to configure a bundle. The simplest form of bundle configuration is to configure the parameters and services inside the application’s main configuration file (app/config/config.yml). This is a totally valid option, although you need to understand the implications of this to better understand when this really is appropriate. Having bundle configuration in the main config file makes your bundle tightly coupled to the current application, and therefore not very portable. This might be okay, if the bundle doesn’t have many services or parameters and you know it will be used only in the current application. This said, I really don’t recommend using this approach even then, because things tend to change and more configuration might be needed later on and other developers will probably look for the configuration inside the bundle.

Another simple method is to have a separate configuration file inside the bundle (Resources/config/services.yml for example) and import it in the main configuration. At the beginning of config.yml there are usually some imports already, so all you need to do is add one more import to the list that points to your bundle configuration file. Note, the path is relative to the main config file. Here is a small example:

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: ../../src/Cvuorinen/ExampleBundle/Resources/config/services.yml }

Loading bundle configuration, the semantic way

As noted above, the extension class handles loading the bundle configuration. It creates an instance of a Configuration class, from the Configuration.php file also located in the DependencyInjection folder. The Configuration class is used to validate and process the main configuration (from config files in app/config/) related to the bundle. If there is no configuration related to your bundle in the main config, the Configuration class can be left as is (more on this after the next paragraph).

After processing the main configuration, the extension class loads the bundle specific configuration from Resources/config folder of the bundle using a loader appropriate for the config file type (YamlFileLoader when using YAML). Any services defined in the bundles configuration file can use the parameters from the applications main config file (and global parameters) and will be added to the service container. After this the services are available for your controllers, CLI commands or any other “container aware” part of the application. You just need to call the get method of the container with the service name specified in the service configuration.

The real power of the semantic configuration comes from the way it can be used to process, merge and validate configuration options from the application’s main configuration and pass those on to the bundles service configuration. This is especially useful when creating bundles that are intended for wider audiences, like open source bundles, but might also be good for in-house distribution inside companies. This way you can provide a few simple and well documented configuration options (with default values where appropriate), that users of the bundle can set in the main configuration file and then the bundles extension class can process and validate them so that the user of the bundle does not need to touch any configuration inside the bundle.

Loading bundle configuration, the right way?

Of course, there is no one right way to do it. But most of the time, the easy way is not the way to go since the whole idea of bundles is to make them reusable across different applications. And it’s also quite easy to auto-generate all the required files for the semantic configuration using the Symfony CLI command:

$ app/console generate:bundle

When first starting development with Symfony, I would say go with the easy way or the (auto-generated) semantic way without any configuration processing. When you get more familiar with the bundle system and start creating bundles that you plan on distributing to other people, then learn more about the validation and merging of the main configuration.

Configuration file structure

Ok, now that we know how to load a configuration file, let’s figure out what to put in there. The configuration consists mainly of two types of information, parameters and services.

Parameters are any static values that your bundle requires. Things like login credentials, API keys, host names or URLs of external services, you probably get the idea. Parameters can be any scalar values, (e.g. strings, numbers, booleans) and arrays and they are specified under the parameters key in the configuration. It is also good practice to specify service class names as parameters, as this allows extending the bundle and, for example, overriding certain services from another bundle. You can retrieve the parameters from the container by calling the getParameter method with the parameter name as an argument, but most of the time you will be passing the parameters as arguments to services in the configuration.

Services are the classes that hold the business logic of your bundle. By defining them in the configuration file, you can harness the power of the dependency injection container, i.e. inject parameters and other services into them automatically, and later retrieve them from the container fully operational with a single line of code. If you are not very familiar with dependency injection, I suggest you read about it a little bit first, there are many good explanations about it elsewhere so I will not cover it in detail here.

By convention, the parameters and services are namespaced with the bundle name (in snake case), but this is not enforced in any way so you are free to name them as you like. Although I would recommend using the bundle name namespacing to avoid any collisions with other bundles.

Here is a small example of a services.yml file from an example bundle:

parameters:
    cvuorinen_example.greeter.class: Cvuorinen\ExampleBundle\Service\Greeter
    cvuorinen_example.greeter.greeting: "Hello"

services:
    cvuorinen_example.greeter:
        class: %cvuorinen_example.greeter.class%
        arguments: [%cvuorinen_example.greeter.greeting%]

Here we define one service (called cvuorinen_example.greeter). We have two parameters, the first one holds the class name of the Greeter service and is set as the class parameter of the service, and the other one is a string value that will be passed to the service as a constructor argument using the arguments parameter array of the service.

As you can see, the defined parameters can be referenced in the configuration by wrapping them with %-characters. You can also reference parameters from the main configuration, and also from other bundles (although be aware that this again makes your bundle coupled to the other bundle). You can read more about parameters in the Symfony documentation.

You can test and debug your configuration with the CLI command:

$ app/console container:debug

This will construct and merge the bundle configurations and print out all the registered services, or an error message in case there is something wrong with the configuration. There is also a --parameters option to print out all the parameters.

Putting it all together

Now that we have a working service configuration, let’s see how this can be used in a controller.

First, here is our very simple Greeter service:

namespace Cvuorinen\ExampleBundle\Service;

class Greeter
{
    public function __construct($greeting)
    {
        $this->greeting = $greeting;
    }

    public function greet($name)
    {
        return $this->greeting . ' ' . $name;
    }
}

In this post I will focus on the service configuration, so I will not cover router configuration in detail. You can read more about it in the official documentation if you are not familiar with it. But suppose we have a route with pattern /hello/{name}, that points to a DefaultController inside our example bundle. Then our controller might look something like this:

namespace Cvuorinen\ExampleBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction($name)
    {
        $greeter = $this->get('cvuorinen_example.greeter');

        return new Response(
            $greeter->greet($name)
        );
    }
}

As you can see, there is no logic in the controller, it only retrieves a service from the container and calls a method on that service and passes along a parameter from the request. The controller does not need to know how to construct the Greeter service, the dependency injection container does all the work for it. This allows us to keep the controllers “skinny” and all the business logic and dependency management elsewhere.

Now, suppose we wanted to change the greeting, all we need to do is change the parameter inside the configuration file. This might not seem like a big deal with this example, but in a real world application you will probably reuse the same services in many places of the application. With this perspective, the benefits are quite obvious.

What about the dependencies!?

Ok, if you have been paying attention, you might have noticed that our example didn’t really inject any dependencies, only a simple parameter. This is true, I wanted to keep the first example as simple as possible. Now, what if we need to make a multilingual application? We will have to use some kind of translation system and the Greeter service needs it to translate the greeting.

First, let’s change the service class like this:

namespace Cvuorinen\ExampleBundle\Service;

use Symfony\Component\Translation\TranslatorInterface;

class Greeter
{
    public function __construct($greeting, TranslatorInterface $translator)
    {
        $this->greeting = $greeting;
        $this->translator = $translator;
    }

    public function greet($name)
    {
        return $this->translator->trans($this->greeting) . ' ' . $name;
    }
}

Then let’s change the configuration to inject a translator service like this:

parameters:
    cvuorinen_example.greeter.class: Cvuorinen\ExampleBundle\Service\Greeter
    cvuorinen_example.greeter.greeting: "Hello"

services:
    cvuorinen_example.greeter:
        class: %cvuorinen_example.greeter.class%
        arguments:
            - %cvuorinen_example.greeter.greeting%
            - @translator

Otherwise this is exactly the same as the previous example, the only difference is in the arguments section of the greeter service configuration. Here we have added another constructor argument, this time it is another service. Services can be referenced in the configuration by prefixing them with the @-character. We are using the default Symfony Translator service, which has the name “translator”. Some core services do not have a namespace in their name, but you could just as well inject a custom translator service with @cvuorinen_example.translator for example.

Injecting all the things

So far I have only covered the “basic” injection method, constructor injection. The Symfony service container can also be used with setter injection and property injection.

Setter injection means that the service class has a separate setter method for a dependency (because it might be optional, for example). This can be configured by adding a calls parameter to the service configuration that is an array of method calls that will be executed after the service has been constructed. Property injection means that the class has public properties and the dependency can be set into it from outside. This can be achieved with properties parameter in the service configuration. More information about the different injection types can be found in the documentation.

The service configuration can also use another service as a factory to create a service. The factory can be another service inside your own bundle, but you can also use this feature with some existing services. One example is that if your service needs a certain Doctrine repository, instead of injecting the entity manager service, you can use the entity manager service as a factory to define the repository class as a service. This way, your service class only needs to add a dependency to the repository class and not the entire entity manager. This makes the service more portable and more easily testable, since you will only need to mock the repository class.

Services can also be declared private. This means they will not be available from the container using the get method, they can only be used as arguments injected to other services. This can be done by setting public: false in the service configuration.

Here is a final example that shows how we can define an entity repository as a service using the Doctrine entity manager as a factory and injecting it into another service using setter injection. The entity repository service is also declared private, so it will not be available through the container.

parameters:
    cvuorinen_example.item_repository.class: Doctrine\ORM\EntityRepository
    cvuorinen_example.item_repository.entity: "CvuorinenExampleBundle:Item"
    cvuorinen_example.items.class: Cvuorinen\ExampleBundle\Service\Items

services:
    cvuorinen_example.item_repository:
        class: %cvuorinen_example.item_repository.class%
        public: false
        factory_service: doctrine.orm.entity_manager
        factory_method: getRepository
        arguments: [%cvuorinen_example.item_repository.entity%]
    cvuorinen_example.items:
        class: %cvuorinen_example.items.class%
        calls:
            - [setRepository, [@cvuorinen_example.item_repository]]

Conclusion

This turned out to be quite a long post and there is still so much more to the Symfony bundle configuration and dependency injection container that I have not covered. Hopefully this will help you understand the Symfony bundle system a little bit better and encourage you to use dependency injection more. I hope to cover more advanced topics in a future post, such as overriding bundle configuration (bundle inheritance), sharing parameters between bundles and defining controllers as services.

Mean while, you can learn more about the service container and dependency injection from the Symfony documentation.

One last thing. Even though the service container itself is a service that can be referenced in the configuration and injected into other services, don’t do that. That is really the anti-pattern of dependency injection, as the services will then be responsible of their own dependencies again and also the services then become tightly coupled to the container itself.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.drlinux.no/ Arne K. Haaje

    Thanks a lot for this detailed and well thought out post!

    I got introduced to Symfony by taking over a clients project, and the learning curve was quite steep. This put some some pieces in their right places in the puzzle that was “services” ;)