Today a friend asked me: Can you recommend a good tutorial on how to create a Symfony bundle?
When I googled (yes, I use it as a verb) how to create a symfony bundle
I was preparing to evaluate some tutorials and recommend one of them.
Surprisingly, I didn’t found any material! I came across The bundle system and Generating a New Bundle Skeleton from the Symfony documentation. But they didn’t even scratch the surface of a bundle creation process. The Doctrine section of the documentation, like most of the topics, is well detailed, with links to more advanced subjects. At the time of writing,
I think that the documentation is of very high quality, but there are still some corners that are not addressed well.

So, here we are. This is step by step tutorial to create a Symfony bundle.

After a while of writing, my scrollbar started to look small on my 30cm hight screen.
The tutorial got too long already and I didn’t cover even a tiny part of the topics I am planning to work with.
So I will split it in smaller tutorials. We will develop a PerformanceMeter Bundle that measures Symfony application’s performance (HTTP requests, queries..).
Here is the breakdown:

  • V0.0.0 (this one)
    • Develop a bare minimum Bundle to log HTTP requests duration
    • Share the bundle on Github and Packagist.org
  • V0.0.1
    • Automated testing
    • Configuration
    • Versioning
  • V0.0.2
    • Integrate with Doctrine
    • Compiler passes
  • V0.0.3
    • Travis-CI, service container
  • V0.0.4
    • Integrate with Twig
    • Console component
  • V0.0.5
    • Monolog Bundle
    • EventDispatcher component

If you want to work on the tutorial as a workshop, which I urge you to do, you can clone the previous branch and start from it. If you want to start with V0.0.1, for instance, you can checkout the V0.0.0 branch and start from there.

PerformanceMeterBundle V0.0.0

For the first iteration, we will just log all HTTP requests duration and URI.

I assume you already have Composer installed, if not, please install it. The initial functionality consists on logging every request.
Thus, we will need 3 packages: The HttpFoundation Component, Stopwatch component and PSR log.
The choice of PSR-log over a concrete logger enables us to be independent of users choice of their logger as long as it implements Psr\Log\LoggerInterface. Let’s create a project directory and start working.

$ mkdir PerformanceMeterBundle

PerformanceMeterBundle/composer.json

{
  "name": "skafandri/performance-meter-bundle",
  "autoload": {
    "psr-4": {
      "Skafandri\\PerformanceMeterBundle\\": "src/"
    }
  },
  "require": {
    "symfony/http-foundation": "^3.2",
    "symfony/stopwatch": "^3.2",
    "psr/log": "^1.0"
  }
}

Run composer install to get all the necessary packages.

$ composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Installing symfony/polyfill-mbstring (v1.3.0)
  Loading from cache

- Installing symfony/http-foundation (v3.2.2)
  Loading from cache

- Installing symfony/stopwatch (v3.2.2)
  Loading from cache

- Installing psr/log (1.0.2)
  Loading from cache

Writing lock file
Generating autoload files

Now we can create the first version of a RequestLogger.

PerformanceMeterBundle/src/RequestLogger.php

<?php
namespace Skafandri\PerformanceMeterBundle;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;

class RequestLogger
{
    /** @var  LoggerInterface */
    private $logger;

    /**
     * RequestLogger constructor.
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    public function logRequest(Request $request, $duration)
    {
        if (!$this->logger) {
            return;
        }
        $context = array(
            'uri' => $request->getUri(),
            'duration' => $duration
        );
        $this->logger->info('performance_meter.request', $context);
    }
}

In order to run the previous code, we would need to write something like:

//Somewhere before a request is processed
$stopWatch = new Stopwatch(); //http://symfony.com/doc/current/components/stopwatch.html
$stopWatch->start('request');
$requestLogger = new RequestLogger($logger);
...
//After a request is processed
$duration = $stopWatch->stop('request')->getDuration();
$requestLogger->logRequest($request, $duration);

Checking the events documentation, it seems that kernel.request and kernel.response are good candidates where we can plug the previous pseudo code.
We need to require http-kernel and event-dispatcher

$ composer require symfony/http-kernel symfony/event-dispatcher
Using version ^3.2 for symfony/http-kernel
Using version ^3.2 for symfony/event-dispatcher
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/debug (v3.2.2)
    Downloading: 100%         
  - Installing symfony/event-dispatcher (v3.2.2)
    Downloading: 100%         
  - Installing symfony/http-kernel (v3.2.2)
    Downloading: 100%         
Writing lock file
Generating autoload files

We’ve got the kernel and an event dispatcher, we can now write an event subscriber.

PerformanceMeterBundle/src/KernelEventsSubscriber.php

<?php
namespace Skafandri\PerformanceMeterBundle;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Stopwatch\Stopwatch;

class KernelEventsSubscriber implements EventSubscriberInterface
{
    /** @var  RequestLogger */
    private $requestLogger;
    /** @var  Stopwatch */
    private $stopwatch;

    /**
     * KernelEventsSubscriber constructor.
     * @param RequestLogger $requestLogger
     */
    public function __construct(RequestLogger $requestLogger)
    {
        $this->requestLogger = $requestLogger;
        $this->stopwatch = new Stopwatch();
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
            $this->stopwatch->start('request');
        }
    }

    public function onKernelResponse(FilterResponseEvent $event)
    {
        $duration = $this->stopwatch->stop('request')->getDuration();
        $this->requestLogger->logRequest($event->getRequest(), $duration);
    }

    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::REQUEST => 'onKernelRequest',
            KernelEvents::RESPONSE => 'onKernelResponse'
        );
    }
}

So we simply start a stopwatch timer on kernel.request, and we log the request duration onkernel.response.

Now we just need to register this subscriber as a service, which leads us to require the dependency injection component.

$ composer require symfony/dependency-injection symfony/config
Using version ^3.2 for symfony/dependency-injection
Using version ^3.2 for symfony/config
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/dependency-injection (v3.2.2)
    Downloading: 100%         
  - Installing symfony/filesystem (v3.2.2)
    Downloading: 100%
  - Installing symfony/config (v3.2.2)
    Loading from cache  

Writing lock file
Generating autoload files

PerformanceMeterBundle/Resources/config/service.xml

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="performance_meter.request_logger" class="Skafandri\PerformanceMeterBundle\RequestLogger">
            <argument type="service" id="logger" on-invalid="null"/>
        </service>
        <service id="performance_meter.kernel_events_subscriber"
                 class="Skafandri\PerformanceMeterBundle\KernelEventsSubscriber">
            <argument type="service" id="performance_meter.request_logger"/>
            <tag name="kernel.event_subscriber"/>
        </service>
    </services>
</container>

on-invalid="null" tells Symfony to pass null to RequestLogger constructor if there is no logger service. In this case, RequestLogger will silently do nothing on kernel response. Check Dependency Injection documentation.
<tag name="kernel.event_subscriber"/> registers a kernel event subsriber. Check Event Dispatcher documentation.
This is a valid dependency injection configuration file. In order to load it, we need to create an extension where we will have access to a ContainerBuilder object. Among many other things, an extension gives access to a ContainerBuilder during configuration loading phase.

PerformanceMeterBundle/src/DependencyInjection/PerformanceMeterExtension.php

<?php
namespace Skafandri\PerformanceMeterBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class PerformanceMeterExtension extends Extension
{

    public function load(array $configs, ContainerBuilder $container)
    {
        $locator = new FileLocator(array(__DIR__ . '/../../Resources/config'));
        $loader = new XmlFileLoader($container, $locator);
        $loader->load('services.xml');
    }
}

We must also create a Bundle class, even if we don’t need it for the moment.

PerformanceMeterBundle/src/PerformanceMeterBundle.php

<?php

namespace Skafandri\PerformanceMeterBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class PerformanceMeterBundle extends Bundle
{
}

This will be the class that users will instantiate in their AppKernel (or WhataverKernel.php).

We are almost done with this iteration’s code, we need a final .gitignore touch because we don’t want to distribute the vendor directory or composer.lock

PerformanceMeterBundle/.gitignore

composer.lock
vendor

Next, we can commit and push the new bundle to github. I created a PerformanceMeterBundle under my github username.

$ git init; git add .gitignore Resources/ composer.json src/
$ git remote add origin git@github.com:skafandri/PerformanceMeterBundle.git
$ git push origin HEAD
Counting objects: 13, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (10/10), done.
Writing objects: 100% (13/13), 2.44 KiB | 0 bytes/s, done.
Total 13 (delta 0), reused 0 (delta 0)
To git@github.com:skafandri/PerformanceMeterBundle.git

The newborn bundle is now available on github. But not yet available to simply work as composer require skafandri/performance-meter-bundle.
To do so, we need to add it to packagist. After you login or create an account (github connect is supported), you just click submit and paste in the bundle’s git URL (git@github.com:skafandri/PerformanceMeterBundle.git in my case). Voila, the bundle is up now at https://packagist.org/packages/skafandri/performance-meter-bundle.

Next, we will create a standard Symfony application and try to require the new bundle. Check the symfony documentation on how to setup Symfony locally. You can use your preferred method, but make sure you have MonologBundle enabled. I recommend to simply use the standard edition for this turorial.

$ symfony new performance-meter-test

 Downloading Symfony...

    5.3 MiB/5.3 MiB ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%

 Preparing project...

 ✔  Symfony 3.2.1 was successfully installed. Now you can:

    * Change your current directory to /home/ilyes/projects/performance-meter-test

    * Configure your application in app/config/parameters.yml file.

    * Run your application:
        1. Execute the php bin/console server:start command.
        2. Browse to the http://localhost:8000 URL.

    * Read the documentation at http://symfony.com/doc

Require performance-meter-bundle

$ composer require skafandri/performance-meter-bundle
 [InvalidArgumentException]                                                                                        
 Could not find package skafandri/performance-meter-bundle at any version for your minimum-stability (stable). Check the package spelling or your minimum-stability

By default, composer sets the minimum-stability to stable (which is good). We will come later to this, for the moment, to be able to require the bundle, we need to add "minimum-stability": "dev" to the root of composer.json in the newly created application and rerun the previous command.

Enable the bundle as usual by adding new Skafandri\PerformanceMeterBundle\PerformanceMeterBundle() to the $bundles array in performance-meter-test/app/AppKernel.php

Voila! Run the PHP built in web server bin/console server:run then visit http://127.0.0.1:8000/ in your browser.
In var/log/app_dev.log you should see an entry similar to

[2017-01-23 05:47:05] app.INFO: performance_meter.request {"uri":"http://127.0.0.1:8000/_wdt/078b04","duration":19} []

We are done with V0.0.0