We will develop a clone of Threes in Symfony. If you ever played 2048, they are very similar. Threes is its predecessor.

Gameplay:

The player slides numbered tiles on a four-by-four grid to combine addends and multiples of three.For example, ones and twos merge to become a single “three” tile, two threes merge into “six”, and two sixes merge into “12”. Swiping the screen up, down, left, or right moves all of the tiles on the grid in that direction and adds a new tile to the grid in the same direction

Before you start, I suggest you give the game a try, you can find a web version on the official website. Just a quick try to get a feeling about the gameplay then come back here.

You’re back, great! Let’s start by creating a standard Symfony application.

composer create-project symfony/framework-standard-edition threes

Hit enter to set default parameters when prompted.

We will develop this game using TDD style, if you are not familiar with TDD, this should be a good exercise to get you started. I recommend that you follow this tutorial as a workshop that you develop in a dev environment. It is not possible to feel the TDD click just by reading.

By default, Symfony standard doesn’t come with phpunit, so let’s require it.

composer require phpunit/phpunit --dev

The game is played on a 4×4 grid board, which gives us a good starting point. So a new board should have 16 empty cells, right? Let’s rewrite this assumption in PHP.

tests/Threes/BoardTest.php

<?php

namespace Tests\Threes;

use PHPUnit\Framework\TestCase;
use Threes\Board;

class BoardTest extends TestCase
{

    public function test_new()
    {
        $board = new Board();

        $this->assertEquals(
            [
                [0, 0, 0, 0],
                [0, 0, 0, 0],
                [0, 0, 0, 0],
                [0, 0, 0, 0],
            ],
            $board->getGrid()
        );
    }
}

The first version of the Board class in src/Threes/Board.php must be straightforward

<?php

namespace Threes;

class Board
{
    private $grid = [
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ];

    public function getGrid()
    {
        return $this->grid;
    }
}

Running the test suite, all good

./vendor/bin/phpunit
PHPUnit 4.8.35 by Sebastian Bergmann and contributors.

.

Time: 72 ms, Memory: 6.00MB

OK (1 test, 1 assertion)

Next, when we slide up, all cells should move up, this will be the second test case.

public function test_up()
{
    $board = new Board();

    $board->setGrid([
        [0, 0, 0, 0],
        [1, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [1, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ],
        $board->getGrid()
    );
}

First, we need to add a grid setter to the Board

public function setGrid(array $grid)
{
    $this->grid = $grid;
}

Then the up method

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            $this->grid[$l-1][$c] = $currentValue;
            $this->grid[$l][$c] = 0;
        }
    }
}

Running the tests confirms this algorithm works. Let’s update the test_up to cover the case where a value is stuck to the top border.

public function test_up()
{
    $board = new Board();

    $board->setGrid([
        [0, 1, 0, 0],
        [1, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [1, 1, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ],
        $board->getGrid()
    );
}
./vendor/bin/phpunit
PHPUnit 4.8.35 by Sebastian Bergmann and contributors.

.F

Time: 80 ms, Memory: 6.00MB

There was 1 failure:

1) Tests\Threes\BoardTest::test_up
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => Array (
         0 => 1
-        1 => 1
+        1 => 0
         2 => 0
         3 => 0
     )
     1 => Array (...)
     2 => Array (...)
     3 => Array (...)
 )

/home/ilyes/projects/threes/tests/Threes/BoardTest.php:46

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

So our initial algorithm did overwrite the [0,1] cell. So if the current value is 0, we can simply exit the loop.

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            if ($currentValue === 0) {
                continue;
            }
            $this->grid[$l - 1][$c] = $currentValue;
            $this->grid[$l][$c] = 0;
        }
    }
}
$ ./vendor/bin/phpunit
PHPUnit 4.8.35 by Sebastian Bergmann and contributors.

..

Time: 74 ms, Memory: 6.00MB

OK (2 tests, 2 assertions)

Next case is when all the vertical column is filled, nothing should move. We can add this situation to the third column of our test.

public function test_up()
{
    $board = new Board();

    $board->setGrid([
        [0, 1, 1, 0],
        [1, 0, 1, 0],
        [0, 0, 1, 0],
        [0, 0, 1, 0],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [1, 1, 1, 0],
            [0, 0, 1, 0],
            [0, 0, 1, 0],
            [0, 0, 1, 0],
        ],
        $board->getGrid()
    );
}

The test fails with

Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
         1 => 0
-        2 => 1
+        2 => 0
         3 => 0
     )
 )

Apparently our algorithm set the cell [2,3] to 0. So we need to check if a cell is empty before overwriting it’s value. The new up method looks like

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            if ($currentValue === 0) {
                continue;
            }
            if ($this->grid[$l - 1][$c] !== 0) {
                continue;
            }
            $this->grid[$l - 1][$c] = $currentValue;
            $this->grid[$l][$c] = 0;
        }
    }
}

Next case is when values are separated by an empty cell. We can use the fourfth column for this.

public function test_up()
{
    $board = new Board();

    $board->setGrid([
        [0, 1, 1, 0],
        [1, 0, 1, 1],
        [0, 0, 1, 0],
        [0, 0, 1, 1],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [1, 1, 1, 1],
            [0, 0, 1, 0],
            [0, 0, 1, 1],
            [0, 0, 1, 0],
        ],
        $board->getGrid()
    );
}

Tests pass, this was covered out of the box.

Next game rule is that two cells with values 1 and 2 should merge into 3.

public function test_up_add()
{
    $board = new Board();

    $board->setGrid([
        [1, 0, 0, 0],
        [2, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [3, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ],
        $board->getGrid()
    );
}

Currently, our algorithm will skip processing if the cell below the current one is not empty if ($this->grid[$l - 1][$c] !== 0) {continue;}. So before continue we need to check if the current value and the next one are 1 and 3 and add them. The if block becomes

if ($this->grid[$l - 1][$c] !== 0) {
    if ($currentValue === 2 && $this->grid[$l - 1][$c] === 1) {
        $this->grid[$l - 1][$c] = $currentValue + $this->grid[$l - 1][$c];
        $this->grid[$l][$c] = 0;
    }
    continue;
}

Test are green, let’s use the second column of the test to include the reverse case, means 3 and 1.

public function test_up_add()
{
    $board = new Board();

    $board->setGrid([
        [1, 2, 0, 0],
        [2, 1, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [3, 3, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ],
        $board->getGrid()
    );
}

The tests fail again, we need to extend the condition to cover this case as well, the extended version looks like

if (
    $currentValue === 2 && $this->grid[$l - 1][$c] === 1
    ||
    $currentValue === 1 && $this->grid[$l - 1][$c] === 2
)

Tests are green, but the up method started to look unreadable.

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            if ($currentValue === 0) {
                continue;
            }
            if ($this->grid[$l - 1][$c] !== 0) {
                if (
                    $currentValue === 2 && $this->grid[$l - 1][$c] === 1
                    ||
                    $currentValue === 1 && $this->grid[$l - 1][$c] === 2
                ) {
                    $this->grid[$l - 1][$c] = $currentValue + $this->grid[$l - 1][$c];
                    $this->grid[$l][$c] = 0;
                }
                continue;
            }

            $this->grid[$l - 1][$c] = $currentValue;
            $this->grid[$l][$c] = 0;
        }
    }
}

Let’s perform some small refactorings to understand it better. We can start by extracting $this->grid[$l - 1][$c] into a local variable $nextValue.

Result:

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            if ($currentValue === 0) {
                continue;
            }
            $nextValue = $this->grid[$l - 1][$c];

            if ($nextValue !== 0) {
                if (
                    $currentValue === 2 && $nextValue === 1
                    ||
                    $currentValue === 1 && $nextValue === 2
                ) {
                    $this->grid[$l - 1][$c] = $currentValue + $nextValue;
                    $this->grid[$l][$c] = 0;
                }
            }

            $this->grid[$l - 1][$c] = $currentValue;
            $this->grid[$l][$c] = 0;
        }
    }
}

Next, let’s replace the second continue with an explicit second if.
Result:

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            if ($currentValue === 0) {
                continue;
            }
            $nextValue = $this->grid[$l - 1][$c];

            if ($nextValue !== 0) {
                if (
                    $currentValue === 2 && $nextValue === 1
                    ||
                    $currentValue === 1 && $nextValue === 2
                ) {
                    $this->grid[$l - 1][$c] = $currentValue + $nextValue;
                    $this->grid[$l][$c] = 0;
                }
            }
            if ($nextValue === 0) {
                $this->grid[$l - 1][$c] = $currentValue;
                $this->grid[$l][$c] = 0;
            }

        }
    }
}

Let’s merge the two embedded ifs into single one, so instead of

if ($nextValue !== 0) {
    if (
        $currentValue === 2 && $nextValue === 1
        ||
        $currentValue === 1 && $nextValue === 2
    ) {
        $this->grid[$l - 1][$c] = $currentValue + $nextValue;
        $this->grid[$l][$c] = 0;
    }
}

we have

if (
    $nextValue !== 0 && (
        $currentValue === 2 && $nextValue === 1
        ||
        $currentValue === 1 && $nextValue === 2
    )
) {
    $this->grid[$l - 1][$c] = $currentValue + $nextValue;
    $this->grid[$l][$c] = 0;
}

Now we can merge the two ifs into a single one with a large condition

Result:

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            if ($currentValue === 0) {
                continue;
            }
            $nextValue = $this->grid[$l - 1][$c];

            if (
                $nextValue === 0 ||
                $nextValue !== 0 && (
                    $currentValue === 2 && $nextValue === 1
                    ||
                    $currentValue === 1 && $nextValue === 2
                )
            ) {
                $this->grid[$l - 1][$c] = $currentValue + $nextValue;
                $this->grid[$l][$c] = 0;
            }
        }
    }
}

Is clear now that the block

if ($currentValue === 0) {
    continue;
}

is not relevant anymore, so we can get rid of it.

Let’s extract the if condition into a private method areMergable

Result:

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $currentValue = $this->grid[$l][$c];
            $nextValue = $this->grid[$l - 1][$c];
            if ($this->areMergeable($nextValue, $currentValue)) {
                $this->grid[$l - 1][$c] = $currentValue + $nextValue;
                $this->grid[$l][$c] = 0;
            }
        }
    }
}

private function areMergeable($nextValue, $currentValue)
{
    return $nextValue === 0 ||
    $nextValue !== 0 && (
        $currentValue === 2 && $nextValue === 1
        ||
        $currentValue === 1 && $nextValue === 2
    );
}

I am tempted to extract a merge method as well

Result:

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $this->mergeUp($l, $c);
        }
    }
}

private function mergeUp($line, $column)
{
    $currentValue = $this->grid[$line][$column];
    $nextValue = $this->grid[$line - 1][$column];
    if ($this->areMergeable($nextValue, $currentValue)) {
        $this->grid[$line - 1][$column] = $currentValue + $nextValue;
        $this->grid[$line][$column] = 0;
    }
}

Now with simple boolean substitution, we can make the areMergeable method more readable.

private function areMergeable($nextValue, $currentValue)
{
    return
        $nextValue === 0
        ||
        in_array([$currentValue, $nextValue], [[1, 2], [2, 1]]);
}

Don’t forget to confirm the correctness of each refactoring step by running the test suite.

Back to our test_up_add now, the next rule says that starting from 3 equal values add together. We use the third column for this case

public function test_up_add()
{
    $board = new Board();

    $board->setGrid([
        [1, 2, 3, 0],
        [2, 1, 3, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [3, 3, 6, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ],
        $board->getGrid()
    );
}

Now it becomes trivial to cover this case, we need an extra simple check in areMergeable method to see if the current and next values are equal.

private function areMergeable($nextValue, $currentValue)
{
    return
        $nextValue === 0
        ||
        in_array([$currentValue, $nextValue], [[1, 2], [2, 1]])
        || $currentValue === $nextValue && $nextValue > 2;
}

Next rule is when there are 3,3,1,2 on a column, only the first 3+3 should merge, we can add this case using the fourfth column in test_up_add

public function test_up_add()
{
    $board = new Board();

    $board->setGrid([
        [1, 2, 3, 3],
        [2, 1, 3, 3],
        [0, 0, 0, 1],
        [0, 0, 0, 2],
    ]);

    $board->up();

    $this->assertEquals(
        [
            [3, 3, 6, 6],
            [0, 0, 0, 1],
            [0, 0, 0, 2],
            [0, 0, 0, 0],
        ],
        $board->getGrid()
    );
}

Again, this was already covered. Now we can try the down feature. The test case will be very similar with test_up with the cells moving down.

public function test_down()
{
    $board = new Board();

    $board->setGrid([
        [0, 0, 1, 1],
        [1, 0, 1, 0],
        [0, 0, 1, 1],
        [0, 1, 1, 0],
    ]);

    $board->down();

    $this->assertEquals(
        [
            [0, 0, 1, 0],
            [0, 0, 1, 1],
            [1, 0, 1, 0],
            [0, 1, 1, 1],
        ],
        $board->getGrid()
    );
}

Let’s have a first try by duplicating up and mergeUp method and change the direction from up to down.

public function down()
{
    for ($l = 2; $l >= 0; $l--) {
        for ($c = 0; $c < 4; $c++) {
            $this->mergeDown($l, $c);
        }
    }
}

private function mergeDown($line, $column)
{
    $currentValue = $this->grid[$line][$column];
    $nextValue = $this->grid[$line + 1][$column];

    if ($this->areMergeable($nextValue, $currentValue)) {
        $this->grid[$line + 1][$column] = $currentValue + $nextValue;
        $this->grid[$line][$column] = 0;
    }
}

Running the tests.. Works! Looking at mergeUp and mergeDown method the only difference is $line - 1 in the first and $line + 1 in the second. It seems that we can pass it as $nextLine argument and merge the two methods into a single merge method. The new method becomes

private function merge($line, $column, $nextLine)
{
    $currentValue = $this->grid[$line][$column];
    $nextValue = $this->grid[$nextLine][$column];

    if ($this->areMergeable($nextValue, $currentValue)) {
        $this->grid[$nextLine][$column] = $currentValue + $nextValue;
        $this->grid[$line][$column] = 0;
    }
}

We get rid of mergeUp and mergeDown and we change the call to merge from up and down method.

public function up()
{
    for ($l = 1; $l < 4; $l++) {
        for ($c = 0; $c < 4; $c++) {
            $this->merge($l, $c, $l - 1);
        }
    }
}

public function down()
{
    for ($l = 2; $l >= 0; $l--) {
        for ($c = 0; $c < 4; $c++) {
            $this->merge($l, $c, $l + 1);
        }
    }
}

What we did so far is that we separated the 3 main concepts into separate methods
– Traversal boundaries and direction
– Merging algorithm
– Merging conditions

It is clear now that the test_down_add will work without any code change, let’s add the test

public function test_down_add()
{
    $board = new Board();

    $board->setGrid([
        [0, 0, 0, 1],
        [0, 0, 0, 2],
        [2, 2, 3, 3],
        [1, 1, 3, 3],
    ]);

    $board->down();

    $this->assertEquals(
        [
            [0, 0, 0, 0],
            [0, 0, 0, 1],
            [0, 0, 0, 2],
            [3, 3, 6, 6],
        ],
        $board->getGrid()
    );
}

Green tests confirms our assumption.

I bet you are now able to add right and left methods and get them right in the first shot. Let’s start from the right

public function test_right()
{
    $board = new Board();

    $board->setGrid([
        [1, 0, 0, 0],
        [0, 0, 0, 1],
        [1, 1, 1, 1],
        [1, 0, 1, 0],
    ]);

    $board->right();

    $this->assertEquals(
        [
            [0, 1, 0, 0],
            [0, 0, 0, 1],
            [1, 1, 1, 1],
            [0, 1, 0, 1],
        ],
        $board->getGrid()
    );
}
public function right()
{
    for ($l = 0; $l < 4; $l++) {
        for ($c = 2; $c >= 0; $c--) {
            $this->merge($l, $c, $l, $c + 1);
        }
    }

}

Like we added $nextLine argument to merge, it is obvious now that we need a $nextColumn one.

private function merge($line, $column, $nextLine, $nextColumn)
{
    $currentValue = $this->grid[$line][$column];
    $nextValue = $this->grid[$nextLine][$nextColumn];

    if ($this->areMergeable($nextValue, $currentValue)) {
        $this->grid[$nextLine][$nextColumn] = $currentValue + $nextValue;
        $this->grid[$line][$column] = 0;
    }
}

up and down methods will simply pass $c as third parameter.

I expect test_right_add to just work now, let’s see

public function test_right_add()
{
    $board = new Board();

    $board->setGrid([
        [0, 0, 2, 1],
        [0, 0, 1, 2],
        [0, 0, 3, 3],
        [1, 2, 3, 3],
    ]);

    $board->right();

    $this->assertEquals(
        [
            [0, 0, 0, 3],
            [0, 0, 0, 3],
            [0, 0, 0, 6],
            [0, 1, 2, 6],
        ],
        $board->getGrid()
    );
}

The left tests and method should be extremely trivial now

public function test_left()
{
    $board = new Board();

    $board->setGrid([
        [0, 1, 0, 0],
        [1, 0, 0, 0],
        [1, 1, 1, 1],
        [0, 1, 0, 1],
    ]);

    $board->left();

    $this->assertEquals(
        [
            [1, 0, 0, 0],
            [1, 0, 0, 0],
            [1, 1, 1, 1],
            [1, 0, 1, 0],
        ],
        $board->getGrid()
    );
}

public function test_left_add()
{
    $board = new Board();

    $board->setGrid([
        [1, 2, 0, 0],
        [2, 1, 0, 0],
        [3, 3, 0, 0],
        [3, 3, 1, 2],
    ]);

    $board->left();

    $this->assertEquals(
        [
            [3, 0, 0, 0],
            [3, 0, 0, 0],
            [6, 0, 0, 0],
            [6, 1, 2, 0],
        ],
        $board->getGrid()
    );
}
public function left()
{
    for ($l = 0; $l < 4; $l++) {
        for ($c = 1; $c < 4; $c++) {
            $this->merge($l, $c, $l, $c - 1);
        }
    }
}

In the next part, we will integrate the Board class into Symfony to be able to play it in a web browser.

We have been working on PerformanceMeterBundle that logs HTTP requests. Another important metric for a web application is database query execution time. Doctrine DBAL is currently part of the Symfony standard edition (required by Doctrine ORM). Doctrine is the most commonly used ORM in Symfony applications that requires a database layer. We will add a new feature that logs queries execution time, if the application is using DoctrineBundle.

PerformanceMeterBundle V0.0.2

Before thinking about this Doctrine stuff, we need to add support for multiple loggers first. Currently, configuring performance_meter: is enough to get it to work. Since we are planning to measure database queries as well, users will need to configure which metrics they are interested in. To support multiple loggers, the bundle should be able to support a configuration like

performance_meter:
  loggers:
    logger1:
    logger2:

We should make sure that:
1. Configuration is loaded
2. Request loggers are registered
3. KernelEventSubscriber is registered
4. KernelEventSubscriber accepts and uses 2 request loggers

Multiple loggers

1. Configuration loading

Replace tests/DependencyInjection/Fixtures/config.yml with the previous configuration, now the tests fail with Unrecognized option "loggers" under "performance_meter". We just need to make the tests pass to ensure the configuration is accepted.
We update src/DependencyInjection/Configuration.php to support the new option, before the return statement, add an option loggers of type array of arrays

$loggers = $root->children()->arrayNode('loggers')->prototype('array');

2. Register request loggers

First, let’s rename Tests\DependencyInjection\PerformanceMeterExtensionTest::test_registers_request_logger() to test_registers_request_loggers since we now expect multiple loggers. Second, change the test case to reflect the new expectation, we just wrap the previous code in a foreach as following

public function test_registers_request_loggers()
{
    $container = $this->createContainer();
    $container->compile();

    foreach (array('logger1', 'logger2') as $logger) {
        $requestLoggerDefinition = $container->getDefinition('performance_meter.request.' . $logger);
        $this->assertEquals(RequestLogger::class, $requestLoggerDefinition->getClass());
        $this->assertEquals(
            array(
                new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE)
            ),
            $requestLoggerDefinition->getArguments()
        );
    }
}

This change should fail the tests with You have requested a non-existent service "performance_meter.request.logger1".
Until now, we defined the request logger service in Resources/config/services.xml. This was a simple method to do it, however now we have a dynamic number of loggers, so the XML method is not a valid option anymore. We need to register those services manually.
The ContainerBuilder passed to the extension’s load method offers a flexible interface to manipulate definitions. Let’s add a method to src/DependencyInjection/PerformanceMeterExtension.php that will programatically register all the loggers

private function registerRequestLoggers(array $config, ContainerBuilder $container)
{
    $loggerReference = new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE);
    foreach ($config['loggers'] as $name => $logger) {
        $id = 'performance_meter.request.' . $name;
        $container->register($id, RequestLogger::class)->addArgument($loggerReference);
    }
}

Add a call to it $this->registerRequestLoggers($config, $container) after $loader->load('services.xml').

3. KernelEventSubscriber

In Tests\DependencyInjection\PerformanceMeterExtensionTest::test_registers_kernel_event_subscriber() we expect the constructor to have one argument: a Reference to performance_meter.request_logger. Which is obviously wrong since we should expect 2 loggers with the sample configuration we have. Instead of changing the constructor argument to an array, we will use a method addLogger for an easier to use interface.
Let’s update the test case to reflect the new expectation. The class is not changing, we only need to change the second assert to:

$this->assertEquals(
    array(
        array(
            'addLogger',
            array(
                new Reference('performance_meter.request.logger1')
            )
        ),
        array(
            'addLogger',
            array(
                new Reference('performance_meter.request.logger2')
            )
        ),
    )
    ,
    $eventSubscriberDefinition->getMethodCalls()
);

We will update registerRequestLoggers and add a method call to addLogger for each logger, the final code should look like this (the 2 added lines are commented):

private function registerRequestLoggers(array $config, ContainerBuilder $container)
{
    $loggerReference = new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE);
    //Get the subscriber definition
    $eventSubscriberDefinition = $container->getDefinition('performance_meter.kernel_events_subscriber');
    foreach ($config['loggers'] as $name => $logger) {
        $id = 'performance_meter.request.' . $name;
        $container->register($id, RequestLogger::class)->addArgument($loggerReference);
        //Add a method call for this logger
        $eventSubscriberDefinition->addMethodCall('addLogger', array(new Reference($id)));
    }
}

4. KernelEventSubscriber accepts multiple loggers

Next, is to make sure KernelEventSubscriber will call all loggers on kernel.response event.
In Tests\KernelEventsSubscriberTest::test_logs_request_on_kernel_response change
$mockRequestLogger->expects($this->once())
to
$mockRequestLogger->expects($this->exactly(2)).
Remove $mockRequestLogger argument when instanciating KernelEventsSubscriber. Add $eventSubscriber->addLogger($mockRequestLogger); twice right after after $eventSubscriber instantiation $eventSubscriber->addLogger($mockRequestLogger); (we add the same logger twice).

The tests should fail miserably, we need to update Skafandri\PerformanceMeterBundle\KernelEventsSubscriber.
1. Remove the request logger from the constructor
2. Rename the property requestLogger to requestLoggers and give it an initial value of array()
3. Add addLogger method

public function addLogger(RequestLogger $logger)
{
    $this->requestLoggers[] = $logger;
}
  1. In onKernelResponse method, replace $this->requestLogger->logRequest($event->getRequest(), $duration); with
foreach ($this->requestLoggers as $logger) {
    $logger->logRequest($event->getRequest(), $duration);
}

Tests are green, all good. Or not? When your tests are green, as they always should be, you are able to clean and refactor without fear. Forget about the did I possibly broke something? feeling. My favorite refactoring technique is deleting code. Nothing can make a code base cleaner than deleting unnecessary code. Let’s delete some code from Resources/config/services.xml
All the performance_meter.request_logger service definition can go away, green tests confirmed.
The EventSubscriber argument can go away, green tests confirmed.

Wait a minute, we just refactored some configuration, that’s not code. I like to answer with a question. Consider the following function:

public function getPort()
{
    $line = explode("\n", file_get_contents('config.cfg'))[1];
    return explode(":", $line)[1];
}

and the following config.cfg

host: hostname
port: 300
memory_limit: 600

A call to getPort should return 300.
If you change 1 to 2 in the first line of the function, getPort will return 600.
If you swap port and memory_limit in the config, getPort will return 600.
Now getPort started to return a different value than before, was it a code change or a configuration change?
My answer: It was a code change.

Multiple logger types

So far we support multiple loggers but we assume they are all RequestLoggers since is the only supported logger. We are about to add an SQLLogger, so the user must configure the metric for each logger. Update the configuration fixture to

performance_meter:
  loggers:
    logger1:
      metric: request
    logger2:
      metric: request
    logger3:
      metric: sql

Running our test suite, it fails with Unrecognized option "metric". To add this option, in src/DependencyInjection/Configuration.php before the return statement, for each item in loggers, add an option metric of type scalar.

$loggers = $root->children()->arrayNode('loggers')->prototype('array');
$loggers->children()->scalarNode('metric');

return $tree;

Now PerformanceMeterExtensionTest::test_registers_kernel_event_subscriber fails because the bundle registered a logger3. To fix it, in src/DependencyInjection/PerformanceMeterExtension.php registerRequestLoggers should only register loggers of metric request. So at the beginning of the foreach, we add a simple check.

if ($logger['metric'] !== 'request') {
    continue;
}

So all we did is: support a metric configuration option for each logger and load RequestLoggers only when metric=request. Now we are ready to start working on an SQLLogger, or any new logger.

SQL logger

When we worked on RequestLogger, we plugged our Logger to the application using an event subscriber. This technique works for most Symfony components and bundles. Most of them dispatch useful events (Requests, Console, Security, Twig..). Doctrine has a lot of useful events as well, except for start/finish query which is what we need now. So a quick tour in Doctrine code might be fruiteful.
We can start by requiring doctrine bundle do get the source code composer require doctrine/doctrine-bundle --dev.
We know that every interaction with the database happens through a connection. So we can start by inspecting Doctrine\DBAL\Connection. The query method’s description says Executes an SQL statement. Seems exactly what we are looking for. Method source at the time of writing:

public function query()
{
    $this->connect();
    $args = func_get_args();

    $logger = $this->_config->getSQLLogger();
    if ($logger) {
        $logger->startQuery($args[0]);
    }
    try {
        switch (func_num_args()) {
            case 1:
                $statement = $this->_conn->query($args[0]);
                break;
            case 2:
                $statement = $this->_conn->query($args[0], $args[1]);
                break;
            default:
                $statement = call_user_func_array(array($this->_conn, 'query'), $args);
                break;
        }
    } catch (\Exception $ex) {
        throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
    }
    $statement->setFetchMode($this->defaultFetchMode);
    if ($logger) {
        $logger->stopQuery();
    }
    return $statement;
}

The interesting lines are:

$logger = $this->_config->getSQLLogger();
$logger->startQuery($args[0]);
$logger->stopQuery();

So if a logger is configured, it will be called before and after a query. $_config is an instance of Doctrine\DBAL\Configuration which has a nice setSQLLogger method, we need to call that method with our logger and it must implement Doctrine\DBAL\Logging\SQLLogger interface. We need to find where this class is being instantiated. A search with the FQCN in the doctrine source leads to vendor/doctrine/doctrine-bundle/Resources/config/dbal.xml. The relevant part is:

<parameter key="doctrine.dbal.configuration.class">Doctrine\DBAL\Configuration</parameter>
<service id="doctrine.dbal.connection.configuration"
  class="%doctrine.dbal.configuration.class%" public="false" abstract="true" />

So DoctrineBundle registers a connection configuration as service, so all we need to do is to find the configuration definition and add the correct method call to it. But there is a little problem: that service is private and abstract. A little bit of more digging, in DoctrineBundle\DependencyInjection\DoctrineExtension::loadDbalConnection the first line looks like

$configuration = $container->setDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name), 
new DefinitionDecorator('doctrine.dbal.connection.configuration'));`

So it will create a configuration service for each connection, we should consider this finding when writing a test case. Later in the Doctrine extension, it also registers a parameter with all connections named doctrine.connections.
The Connection class accepts a single logger. Since we need multiple loggers, we will have to pass a class that implements SQLLogger but supports multiple loggers. Luckily there is Doctrine\DBAL\Logging\LoggerChain which like it’s name suggests, chains multiple loggers.

So we know what we have to do:
1. Our SQLLogger should implement Doctrine\DBAL\Logging\SQLLogger interface.
2. Register a LoggerChain with references to all loggers.
3. Add a call configuration->setSQLLogger with a reference to our LoggerChain.

1. SQLLogger interface implementation

The interface is very clear, having only 2 methods startQuery and stopQuery.On stopQuery, we expect our logger to log an info with a context matching the arguments passed to startQuery. And it shouldn’t break in case there is no logger.

tests/SQLLoggerTest.php

<?php

namespace Skafandri\PerformanceMeterBundle\Tests;

use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Skafandri\PerformanceMeterBundle\SQLLogger;

class SQLLoggerTest extends TestCase
{
    public function test_logs_request()
    {
        $mockLogger = $this->getMockBuilder(LoggerInterface::class)->getMock();
        $mockLogger->expects($this->once())
            ->method('info')
            ->with(
                'performance_meter.sql_query',
                $this->callback(function ($context) {
                    return
                        'sql' === $context['sql'] &&
                        array('params') === $context['params'] &&
                        is_numeric($context['duration']);
                })
            );

        $sqlLogger = new SQLLogger($mockLogger);
        $sqlLogger->startQuery('sql', array('params'));
        $sqlLogger->stopQuery();
    }

    public function test_doesnt_break_without_logger()
    {
        $sqlLogger = new SQLLogger();
        $sqlLogger->startQuery('sql', array('params'));
        $sqlLogger->stopQuery();
    }
}

The implementation is only slightly different from RequestLogger

src/SQLLogger.php

<?php

namespace Skafandri\PerformanceMeterBundle;

use Psr\Log\LoggerInterface;
use Symfony\Component\Stopwatch\Stopwatch;

class SQLLogger implements \Doctrine\DBAL\Logging\SQLLogger
{

    /** @var LoggerInterface */
    private $logger;

    /** @var Stopwatch */
    private $stopwatch;

    private $sql;
    private $params;

    /**
     * SQLLogger constructor.
     * @param LoggerInterface|null $logger
     */
    public function __construct(LoggerInterface $logger = null)
    {
        $this->logger = $logger;
        $this->stopwatch = new Stopwatch();
    }

    /**
     * @inheritdoc
     */
    public function startQuery($sql, array $params = null, array $types = null)
    {
        $this->sql = $sql;
        $this->params = $params;
        $this->stopwatch->start('query');
    }

    /**
     * @inheritdoc
     */
    public function stopQuery()
    {
        if (!$this->logger) {
            return;
        }
        $duration = $this->stopwatch->stop('query')->getDuration();
        $context = array(
            'sql' => $this->sql,
            'params' => $this->params,
            'duration' => $duration
        );
        $this->logger->info('performance_meter.sql_query', $context);
    }
}

2. Register a LoggerChain

It must be of class LoggerChain and have a method call to addLogger with logger3. We add the test case to tests/DependencyInjection/PerformanceMeterExtensionTest.php

public function test_registers_logger_chain()
{
    $container = $this->createContainer();
    $container->compile();

    $loggerChainDefinition = $container->getDefinition('performance_meter.logger_chain');

    $this->assertEquals(LoggerChain::class, $loggerChainDefinition->getClass());
    $this->assertEquals(
        array(
            array(
                'addLogger',
                array(
                    new Reference('performance_meter.sql.logger3')
                )
            ),
        )
        ,
        $loggerChainDefinition->getMethodCalls()
    );
}

The first test fails with You have requested a non-existent service "performance_meter.logger_chain", we just need to register the service in Resources/config/services.xml <service id="performance_meter.logger_chain" class="Doctrine\DBAL\Logging\LoggerChain"/>. Now it fails to assert that getMethodCalls returns the expected calls. We will add registerSQLLoggers method in src/DependencyInjection/PerformanceMeterExtension.php

private function registerSQLLoggers(array $config, ContainerBuilder $container)
{
    $loggerReference = new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE);
    $loggerChainDefinition = $container->getDefinition('performance_meter.logger_chain');
    foreach ($config['loggers'] as $name => $logger) {
        if ($logger['metric'] !== 'sql') {
            continue;
        }
        $id = 'performance_meter.sql.' . $name;
        $container->register($id, SQLLogger::class)->addArgument($loggerReference);
        $loggerChainDefinition->addMethodCall('addLogger', array(new Reference($id)));
    }
}

and add a call to it right after $this->registerRequestLoggers.

3. Inject LoggerChain in connection configurations

Remember that DoctrineBundle will register a configuration service for each connection. Let’s first append a doctrine configuration to our fixture config.yml

doctrine:
  dbal:
    connections:
      conn1:
      conn2:

If we run the test suite after this change, it would fail with There is no extension able to load the configuration for "doctrine". That’s simply because the Doctrine extension is not loaded. To load it, add $container->registerExtension(new DoctrineExtension()); after $container->registerExtension(new PerformanceMeterExtension()); in PerformanceMeterExtensionTest::createContainer. Now the tests pass again.

Remember that DoctrineBundle registers configuration services with sprintf('doctrine.dbal.%s_connection.configuration', $name).
The expected result is straight forward, 'doctrine.dbal.conn1_connection.configuration' and 'doctrine.dbal.conn2_connection.configuration' should both have performance_meter.logger_chain as SQLLogger. We add another test case to PerformanceMeterExtensionTest

public function test_calls_doctrine_connection_configuration_setSQLLogger_with_logger_chain()
{
    $container = $this->createContainer();
    $container->compile();

    foreach (array('conn1', 'conn2') as $name) {
        $configurationDefinition = $container->getDefinition('doctrine.dbal.' . $name . '_connection.configuration');
        $this->assertEquals(
            array(
                array(
                    'setSQLLogger',
                    array(
                        new Reference('performance_meter.logger_chain')
                    )
                ),
            )
            ,
            $configurationDefinition->getMethodCalls()
        );
    }
}

The assert obviously fails. Let’s start with a first attempt, we know how to manipulate definitions and how to add method calls. Also DoctrineBundle registers a parameter with all connections named doctrine.connections. So in PerformanceMeterExtension::load, after $this->registerSQLLoggers($config, $container); we can add

foreach ($container->getParameter('doctrine.connections') as $connection) {
  $container->getDefinition($connection . '.configuration')
      ->addMethodCall('setSQLLogger', array(new Reference('performance_meter.logger_chain')));
}

The tests fail with You have requested a non-existent parameter "doctrine.connections". Why is that? To understand what happened let’s take a look at Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass which has a single method. But it has a lot of knowledge about Symfony bundle development.

public function process(ContainerBuilder $container)
{
    $parameters = $container->getParameterBag()->all();
    $definitions = $container->getDefinitions();
    $aliases = $container->getAliases();
    $exprLangProviders = $container->getExpressionLanguageProviders();

    foreach ($container->getExtensions() as $extension) {
        if ($extension instanceof PrependExtensionInterface) {
            $extension->prepend($container);
        }
    }

    foreach ($container->getExtensions() as $name => $extension) {

        if (!$config = $container->getExtensionConfig($name)) {
            // this extension was not called
            continue;
        }
        $config = $container->getParameterBag()->resolveValue($config);

        $tmpContainer = new ContainerBuilder($container->getParameterBag());
        $tmpContainer->setResourceTracking($container->isTrackingResources());
        $tmpContainer->addObjectResource($extension);

        foreach ($exprLangProviders as $provider) {
            $tmpContainer->addExpressionLanguageProvider($provider);
        }

        $extension->load($config, $tmpContainer);

        $container->merge($tmpContainer);
        $container->getParameterBag()->add($parameters);
    }

    $container->addDefinitions($definitions);
    $container->addAliases($aliases);
}

I recommend that you navigate (aka Ctrl-click) everything is inspect what it is doing. We can already learn couple of things from the method body.

  1. If an extension (read: bundle) doesn’t have a config (entry in config.yml), it won’t be loaded.
  2. $config = $container->getExtensionConfig($name)
    Each extension is loaded only with it’s own configuration, means it won’t see other bundles configs.
  3. $tmpContainer = new ContainerBuilder($container->getParameterBag()); then $container->merge($tmpContainer);
    Each extension will receive a copy of ContainerBuilder that will be merged later to the main ContainerBuilder. This means that extensions won’t be able to see each other definitions. This is why our test failed then!
  4. This class implements CompilerPassInterface, so if we do the same, we can have access to a full ContainerBuilder, right?

Let’s try. We will simply cut the code snippet we added to the extension and didn’t work, and add it to the new class.

src/DependencyInjection/Compiler/SetSQLLoggerPass.php

<?php
namespace Skafandri\PerformanceMeterBundle\DependencyInjection\Compiler;

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

class SetSQLLoggerPass implements CompilerPassInterface
{

    public function process(ContainerBuilder $container)
    {
      foreach ($container->getParameter('doctrine.connections') as $connection) {
          $container->getDefinition($connection . '.configuration')
              ->addMethodCall('setSQLLogger', array(new Reference('performance_meter.logger_chain')));
      }
    }
}

To load this compiler we need to override the Bundle build method in src/PerformanceMeterBundle.php

public function build(ContainerBuilder $container)
{
    parent::build($container);
    $container->addCompilerPass(new SetSQLLoggerPass());
}

An to simulate how the kernel build a container, we need to add to PerformanceMeterExtensionTest:createContainer, just before the return statement

$performanceMeterBundle = new PerformanceMeterBundle();
$performanceMeterBundle->build($container);

Previous: Part 2

In How to create a Symfony bundle part 1 we developed a minimal bundle that logs every HTTP request. In this iteration we will add some more functionality then release V0.0.1
We will add the feature to enable/disable the bundle through configuration.

PerformanceMeterBundle V0.0.1

When we implemented the request logging, we required the bundle in a test application to test it. The more we will add functionalities, the more the manual testing method becomes impractical. If you work within a team, automated testing becomes even vital to the success of any non trivial project.

First, we need to prepare the required pieces for a testing environment. We will store the tests in tests directory, so create the directory. Add an autoload block to the root of composer.json

"autoload-dev": {
    "psr-4": {
      "Skafandri\\PerformanceMeterBundle\\Tests\\": "tests/"
    }
  },

Require phpunit

$ composer require --dev phpunit/phpunit

Configure phpunit

phpunit.xml.dist

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="PerformanceMeterBundle Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory>.</directory>
            <exclude>
                <directory>Resources</directory>
                <directory>tests</directory>
                <directory>vendor</directory>
            </exclude>
        </whitelist>
    </filter>
</phpunit>

The whitelist filter is required to generate a code coverage report, we will use it later.

Check if everything is OK

$ ./vendor/bin/phpunit
PHPUnit 5.7.6 by Sebastian Bergmann and contributors.

Time: 24 ms, Memory: 2.00MB

No tests executed!

No tests executed! means all is good, we can start writing the first test case.

We will simply write the test cases in the same order we produced the bundle files. The first test for RequestLogger should be trivial.

tests/RequestLoggerTest.php

<?php
namespace Skafandri\PerformanceMeterBundle\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Skafandri\PerformanceMeterBundle\RequestLogger;
use Symfony\Component\HttpFoundation\Request;

class RequestLoggerTest extends TestCase
{
    public function test_logs_request()
    {
        $mockLogger = $this->getMockBuilder(LoggerInterface::class)->getMock();
        $mockLogger->expects($this->once())
            ->method('info')
            ->with(
                'performance_meter.request',
                array('uri' => 'http://:/', 'duration' => 10)
            );

        $requestLogger = new RequestLogger($mockLogger);
        $requestLogger->logRequest(new Request(), 10);
    }

    public function test_doesnt_break_without_logger()
    {
        $requestLogger = new RequestLogger();
        $requestLogger->logRequest(new Request(), 10);
    }
}

The second test has no assertions, isn’t this a bad practice? This test is simply a safeguard against a possible optimization to the block

if (!$this->logger) {
    return;
}

in RequestLogger. PHPUnit doesn’t have an assertion similar to assertEverythingIsFine() and I wouldn’t use it anyway. The function name should be self explanatory.
Needless to say that every time you change the code, you run ./vendor/bin/phpunit to execute the test suite.

Second test case will be to test KernelEventsSubscriber, the scenario is:

-1- create an event subscriber with a mocked request logger
-2- call eventSubscriber->onKernelRequest with a mocked event
-3- call eventSubscriber->onKernelResponse with a mocked event
-*- make sure request logger was called properly

tests/KernelEventsSubscriberTest.php

<?php
namespace Skafandri\PerformanceMeterBundle\Tests;
use PHPUnit\Framework\TestCase;
use Skafandri\PerformanceMeterBundle\KernelEventsSubscriber;
use Skafandri\PerformanceMeterBundle\RequestLogger;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class KernelEventsSubscriberTest extends TestCase
{
    public function test_logs_request_on_kernel_response()
    {
        $request = new Request();

        $mockGetResponseEvent = $this->getGetResponseEventMock();
        $mockGetResponseEvent->expects($this->any())
            ->method('getRequestType')
            ->willReturn(HttpKernelInterface::MASTER_REQUEST);

        $mockFilterResponseEvent = $this->getFilterResponseEventMock();
        $mockFilterResponseEvent->expects($this->any())
            ->method('getRequest')
            ->willReturn($request);

        $mockRequestLogger = $this->getMockBuilder(RequestLogger::class)->getMock();
        $mockRequestLogger->expects($this->once())
            ->method('logRequest')
            ->with($this->callback(function ($requestArgument) use ($request) {
                return $requestArgument === $request;
            }));

        $eventSubscriber = new  KernelEventsSubscriber($mockRequestLogger);
        $eventSubscriber->onKernelRequest($mockGetResponseEvent);
        $eventSubscriber->onKernelResponse($mockFilterResponseEvent);
    }

    private function getGetResponseEventMock()
    {
        return $this
            ->getMockBuilder(GetResponseEvent::class)
            ->disableOriginalConstructor()
            ->getMock();
    }

    private function getFilterResponseEventMock()
    {
        return $this
            ->getMockBuilder(FilterResponseEvent::class)
            ->disableOriginalConstructor()
            ->getMock();
    }
}

The test code seems bigger than the tested code, isn’t it? Yes, and it may get even bigger. A ratio of 2:1 or more is not uncommon. That’s the price you pay to buy this safety net.
In fact, you can estimate the worthiness of automated testing in a particular project:

if (time_to_write_automated_tests + time_to_run_automated_tests*N < time_to_run_manual_tests*N)
    you should write automated tests
else    
    you should do manual testing
endif

Where N is the number of times tests will run, automatically or manually.

Notice that the more N goes up, the less time_to_write_automated_tests is affecting the balance. When N is too big, which is the case in most projects, you can simplify it by just comparing time_to_run_automated_tests to time_to_run_manual_tests.

After the event subscriber, we created services.xml and the extension class. The extension just loads the configuration file into a container, the test scenario is:

-1- Create a container
-2- Register the extension
-*- Make sure request logger is registered with the correct class and arguments
-*- Make sure events subscriber is registered with the correct class, arguments and tags

tests/DependencyInjection/Fixtures/config.yml

performance_meter:

tests/DependencyInjection/PerformanceMeterExtensionTest.php

<?php

namespace Skafandri\PerformanceMeterBundle\Tests\DependencyInjection;

use PHPUnit\Framework\TestCase;
use Skafandri\PerformanceMeterBundle\DependencyInjection\PerformanceMeterExtension;
use Skafandri\PerformanceMeterBundle\KernelEventsSubscriber;
use Skafandri\PerformanceMeterBundle\RequestLogger;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;

class PerformanceMeterExtensionTest extends TestCase
{
    public function test_registers_request_logger()
    {
        $container = $this->createContainer();
        $container->compile();

        $requestLoggerDefinition = $container->getDefinition('performance_meter.request_logger');

        $this->assertEquals(RequestLogger::class, $requestLoggerDefinition->getClass());
        $this->assertEquals(
            array(
                new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE)
            ),
            $requestLoggerDefinition->getArguments()
        );
    }

    public function test_registers_kernel_event_subscriber()
    {
        $container = $this->createContainer();
        $container->compile();

        $eventSubscriberDefinition = $container->getDefinition('performance_meter.kernel_events_subscriber');

        $this->assertEquals(KernelEventsSubscriber::class, $eventSubscriberDefinition->getClass());
        $this->assertEquals(
            array(
                new Reference('performance_meter.request_logger')
            ),
            $eventSubscriberDefinition->getArguments()
        );
        $this->assertEquals(
            array('kernel.event_subscriber' => array(array())),
            $eventSubscriberDefinition->getTags()
        );
    }

    private function createContainer()
    {
        $container = new ContainerBuilder();

        $locator = new FileLocator(__DIR__.'/Fixtures');
        $loader = new YamlFileLoader($container, $locator);
        $loader->load('config.yml');

        $container->registerExtension(new PerformanceMeterExtension());

        $container->getCompilerPassConfig()->setOptimizationPasses(array());

        return $container;
    }
}

I think the previous example beats all the quests to find the golden number that defines a good test coverage percentage, and must be between 0 and 100.
PHPUnit will think the previous test is testing PerformanceMeterExtension, which has 3 lines of code. But in reality, we are testing services.xml.

We are done testing our previous code, we can proceed to implement the next feature. The user should be able to activate/deactivate the bundle using a configuration toggle.

performance_meter:
    enabled: true|false defaults to true

Before thinking about how to implement this feature, let’s think how to test it first. We can create a dummy configuration file with enabled: false and make sure the bundle is disabled. The scenario looks like:

-1- Create a container
-2- load disabled.yml
-*- check that no services are loaded

tests/DependencyInjection/Fixtures/disabled.yml

performance_meter:
    enabled: false

in tests/DependencyInjection/PerformanceMeterExtensionTest.php we add another test case

public function test_registers_nothing_when_disabled()
{
    $container = $this->createContainer();

    $locator = new FileLocator(__DIR__ . '/Fixtures');
    $loader = new YamlFileLoader($container, $locator);
    $loader->load('disabled.yml');

    $container->compile();

    $this->assertFalse($container->has('performance_meter.request_logger'));
    $this->assertFalse($container->has('performance_meter.kernel_events_subscriber'));
}

Running the test suite again, it fails obviously

$ ./vendor/bin/phpunit
PHPUnit 5.7.6 by Sebastian Bergmann and contributors.

..F...                                                              6 / 6 (100%)

Time: 207 ms, Memory: 6.00MB

There was 1 failure:

1) Skafandri\PerformanceMeterBundle\Tests\DependencyInjection\PerformanceMeterExtensionTest::test_registers_nothing_when_disabled
Failed asserting that true is false.

/home/ilyes/projects/PerformanceMeterBundle/tests/DependencyInjection/PerformanceMeterExtensionTest.php:63

FAILURES!
Tests: 6, Assertions: 8, Failures: 1.

Once we manage to pass the tests, we will be done with this feature.

PerformanceMeterExtension seems a good candidate, all we need is to check for the enabled configuration and call $loader->load('services.xml') only when it has the value true.
The first argument $configs for the load method is a raw array of configs defined by users and appended by other bundles. This array needs to be processed using a Configuration class that defines how configuration should be.

So let’s create the configuration class.
src/DependencyInjection/Configuration.php

<?php
namespace Skafandri\PerformanceMeterBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{

    /**
     * Generates the configuration tree builder.
     *
     * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder
     */
    public function getConfigTreeBuilder()
    {
        $tree = new TreeBuilder();
        $tree->root('performance_meter');

        return $tree;
    }
}

We will use it from PerformanceMeterExtension to process the $configs array. Before $loader->load('services.xml'), insert $config = $this->processConfiguration(new Configuration(), $configs);

Run the tests again, now we get an exception Symfony\Component\Config\Definition\Exception\InvalidConfigurationException: Unrecognized option "enabled" under "performance_meter". We need to add to the TreeBuilder returned from the Configuration class expect a key of type scalar and a default value true. Edit the configuration class, assign the root node creation to a variable $root = $tree->root('performance_meter'); and add the enabled node $root->children()->scalarNode('enabled')->defaultTrue();.

Run the tests again, we get back to Failed asserting that true is false. Since now we have the new config recognized and parsed, we update PerformanceMeterExtension to load services.xml only when enabled=true, we just wrap it in an if clause

if ($this->isConfigEnabled($container, $config)) {
    $loader->load('services.xml');
}

Run the tests again.. all good.

If you haven’t done it before, congratulations, this was your first TDD session.

In the previous tutorial we had to add "minimum-stability": "dev" to the test application in order to be able to require performance-meter-bundle. The minimum stability defaults to stable, which from composer’s perspective, it just means that the project has some specifically named git tags. The tags represent Semantic Versioning.
To release the first stable version of this bundle, we need to add a version tag v0.0.1

$ git add .; git commit -m "V0.0.1"; git tag v0.0.1.

Before pushing the changes to Github, you can go to the project settings on Github, Integrations & services and add Packagist from the list. Now changes pushed to Github will automatically update the project on Packagist through a webhook.

$ git push origin HEAD --tags

Next: How to create a Symfony bundle part 3
Previous: How to create a Symfony bundle part 1

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