Loïc Faugeron Technical Blog

The Ultimate Developer Guide to Symfony - Bundle 09/03/2016

Reference: This article is intended to be as complete as possible and is kept up to date.

TL;DR: Configure services from a third party library in a Bundle.

In this guide we've explored the main standalone libraries (also known as "Components") provided by Symfony to help us build applications:

In this article, we're going to have a closer look at how HttpKernel enables reusable code.

Then in the next article we'll see the different ways to organize our application tree directory.

Finally we'll finish by putting all this knowledge in practice by creating a "fortune" project with:

HttpKernel vs Kernel

The HttpKernel component provides two implementations for HttpKernelInterface.

The first one, HttpKernel, relies on Event Dispatcher and Routing to execute the appropriate controller for the given Request.

And the second one, Kernel, relies on Dependency Injection and HttpKernel:

<?php

namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;

class Kernel implements HttpKernelInterface
{
    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        if (false === $this->booted) {
            $this->boot();
        }

        return $this->container->get('http_kernel')->handle($request, $type, $catch);
    }

    public function boot()
    {
        // Initializes the container
    }

    abstract public function registerBundles();
}

Note: For brevity's sake, Kernel has been heavily truncated.

Initialization of the container includes:

  1. retrieving all "bundles"
  2. creating a ContainerBuilder
  3. for each bundles:
    1. registering its ExtensionInterface implementations in the container
    2. registering its CompilerPassInterface implementations in the container
  4. dumping the container in an optimized implementation

Once the container is initialized, Kernel expects it to contain a http_kernel service to which it will delegate the actual HTTP work.

Bundle

A bundle is a package that contains ExtensionInterface and CompilerPassInterface implementations, to configure a Dependency Injection container. It can be summed up by this interface:

<?php

namespace Symfony\Component\HttpKernel\Bundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;

interface BundleInterface
{
    // Adds CompilerPassInterface implementations to the container
    public function build(ContainerBuilder $container);

    // Returs an ExtensionInterface implementation, which will be registered in the container
    public function getContainerExtension();
}

Note: Once again, this interface has been truncated for brevity's sake.

Bundles are usually created for one of the following purposes:

Note: the last category is considered bad practice, as explained in the following, articles:

Bundles follow by convention the following directory tree:

.
├── Command
├── Controller
├── DependencyInjection
│   └── CompilerPass
├── EventListener
├── Resources
│   └── config
│       └── services
│           └── some_definitions.yml
├── Tests
└── VendorProjectBundle.php

NanoFrameworkBundle example

Since HttpKernel component is a third party library, we're going to create a bundle to provide its classes as Dependency Injection services. This is also a good opportunity to have a look at how a Symfony application works behind the hood.

NanoFrameworkBundle's purpose is to provides a http_kernel service that can be used by Kernel. First let's create a directory:

mkdir nano-framework-bundle
cd nano-framework-bundle

Then we can create an implementation of BundleInterface:

<?php
// VendorNanoFrameworkBundle.php

namespace Vendor\NanoFrameworkBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class VendorNanoFrameworkBundle extends Bundle
{
}

Bundle extension

To be able to load Dependency Injection configuration, we'll create an implementation of ExtensionInterface:

<?php
// DependencyInjection/VendorNanoFrameworkExtension.php

namespace Vendor\NanoFrameworkBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\DirectoryLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class VendorNanoFrameworkExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $fileLocator = new FileLocator(__DIR__.'/../Resources/config');
        $loader = new DirectoryLoader($container, $fileLocator);
        $loader->setResolver(new LoaderResolver(array(
            new YamlFileLoader($container, $fileLocator),
            $loader,
        )));
        $loader->load('services/');
    }
}

Once done, we can create the configuration:

# Resources/config/services/http_kernel.yml
services:
    http_kernel:
        class: Symfony\Component\HttpKernel\HttpKernel
        arguments:
            - "@event_dispatcher"
            - "@controller_resolver"
            - "@request_stack"

    event_dispatcher:
        class: Symfony\Component\EventDispatcher\EventDispatcher

    controller_resolver:
        class: Symfony\Component\HttpKernel\Controller\ControllerResolver
        public: false

    request_stack:
        class: Symfony\Component\HttpFoundation\RequestStack

Bundle compiler pass

In order to register event listeners in EventDispatcher in a way that doesn't require us to edit Resources/config/services/http_kernel.yml, we're going to create an implementation of CompilerInterface:

<?php
// DependencyInjection/CompilerPass/AddListenersPass.php

namespace Vendor\NanoFrameworkBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class AddListenersPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $eventDispatcher = $container->findDefinition('event_dispatcher');
        $eventListeners = $container->findTaggedServiceIds('kernel.event_listener');
        foreach ($eventListeners as $id => $events) {
            foreach ($events as $event) {
                $eventDispatcher->addMethodCall('addListener', array(
                    $event['event'],
                    array(new Reference($id), $event['method']),
                    isset($event['priority']) ? $event['priority'] : 0;
                ));
            }
        }
    }
}

With this, we only need to add a tag with:

To complete the step, we need to register it in our bundle:

<?php
// VendorNanoFrameworkBundle.php

namespace Vendor\NanoFrameworkBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Vendor\NanoFrameworkBundle\DependencyInjection\CompilerPass\AddListenersPass;

class VendorNanoFrameworkBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new AddListenersPass());
    }
}

Note: While CompilerPassInterface implementations need to be registered explicitly, there is no need to do anything for ExtensionInterface implementations as Bundle contains a method able to locate it, based on the following conventions:

  • it needs to be in DependencyInjection directory
  • it needs to be named after the bundle name (replace Bundle suffix by Extension)
  • it needs to implement ExtensionInterface

More configuration

HttpKernel relies on event listeners for the routing, in order to enable it we need to add the following configuration:

# Resources/config/services/routing.yml
services:
    router_listener:
        class: Symfony\Component\HttpKernel\EventListener\RouterListener
        arguments:
            - "@router"
            - "@request_stack"
            - "@router.request_context"
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 32 }

    router:
        class: Symfony\Component\Routing\Router
        public: false
        arguments:
            - "@routing.loader"
            - "%kernel.root_dir%/config/routings"
            - "%router.options%"
            - "@router.request_context"
        calls:
            - [setConfigCacheFactory, ["@config_cache_factory"]]

    routing.loader:
        class: Symfony\Component\Config\Loader\DelegatingLoader
        public: false
        arguments:
            - "@routing.resolver"

    routing.resolver:
        class: Symfony\Component\Config\Loader\LoaderResolver
        public: false
        calls:
            - [addLoader, ["@routing.loader.yml"]]

    router.request_context:
        class: Symfony\Component\Routing\RequestContext
        public: false

    config_cache_factory:
        class: Symfony\Component\Config\ResourceCheckerConfigCacheFactory
        public: false

    routing.loader.yml:
        class: Symfony\Component\Routing\Loader\YamlFileLoader
        public: false
        arguments:
            - "@file_locator"

Usage

Since Kernel is an abstract class, we need to create an implementation (usually called AppKernel):

<?php
// Tests/app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        return array(
            new Vendor\NanoFrameworkBundle\VendorNanoFrameworkBundle(),
        );
    }

    public function getRootDir()
    {
        return __DIR__;
    }

    public function getCacheDir()
    {
        return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
    }

    public function getLogDir()
    {
        return dirname(__DIR__).'/var/logs';
    }
}

Finally we need to create a "Front Controller" (a fancy name for index.php):

<?php
// Tests/web/index.php

<?php

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Conclusion

Bundles enable us to define classes as Dependency Injection services, for our applications and third part libraries in a reusable way.

In the example above we've created a bundle that provides a http_kernel service, which can then be used to create Symfony applications. Here are some existing bundles that do it for us:

There are many bundles available, you can find them by checking symfony-bundle in Packagist.