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.

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

After we moved to a new address, 3 things needed to happen:

  • change my wife’s ID
  • change my staying permit
  • change the car ID

Since I don’t have much knowledge about government administration, I decided to model the process as software and code-review it to see how efficient it is.

The process is the following:

  • When a citizen’s address changes, the new address and a proof of address are required to update his ID.
  • When the ID is created, the citizen is notified (I invented this feature to make the process easier to design, the reality resembles more to a sleep/retry loop).
  • When a citizen’s address changes, the new address and a proof of address are required to update his resident partner’s staying permit.
  • When a citizen’s address changes, the new address and a proof of address are required to update his car registration document.
  • When the car registration document is created, the citizen is notified (invented).
  • When a resident’s address changes, the new address and a proof of address are required to update his staying permit.
  • When staying permit is created, resident is notified about staying permit ready (invented, see above).

I can’t emphasize enough how much I simplified the process. The real version is a bit more complicated.

Next, I will try to model this process as a flow chart.

 

I will suppose we are building a software, and a junior developer sent me this initial attempt for code review.

At first glance, it seems that Citizen’s address changed event is requiring 3 actions that are performed at 3 different targets. This is clearly redundant. We can solve this issue in two ways:

Solution 1: Hide all the involved targets behind a single unified interface, something like Citizens affairs Administration. This new administration will be the single contact point with citizens and will handle distributing the event to the interested administration.

Solution 2: Pick one of the existing administrations, the most powerful, the most secure or however you play your politics, and make it act as the unified interface to the citizens.

From software infrastructure point of view, Solution 1 is not the best. It will require new machines to provision, install, develop and run software on. In a governmental institution context, this would mean a whole bunch of construction projects, boring news, and endless nonsense debates on TV. Let’s proceed with Solution 2.

The refactored process :

 

This variation reduces the required actions for a Citizen from 3 to 1. We will suppose we only reduced it from 2 to 1, because not everybody has a car neither everybody has a foreign partner. Although some other people would need other documents to change if they change their addresses, the 2 to 1 average seems a fair compromise. What does that mean in human terms?

I will put a lot of over simplifications on the following estimations:

– Today, there are about 20 million people in Romania.
– On average, a citizen will change his address once in 50 years (read as: on average, a citizen is most likely to never change his address, maybe change it once, but rarely more than once)
– This means in the next 50 years we should expect an average of 400000 address changes/year
– If an optimization reduces 1 hour for each citizen, the population would save 400000 hours/year

That’s 45 years, or the equivalent of a team of 200 people working a full year without taking vacations.
How hard it is, in theory, to shave only 10 hours/year from a citizen’s time spent in administrations in your country?

commodore 64

    commodore 64

Sometime around 1994, my father bought me a Commodore 64. I quickly got bored of the few games I had, and God! It took ages to load a game from a tape. And if I miss-typed the game address, I had to rewind the tape and start over again. There was a small local computer club in my neighborhood. There were kids playing games like I did, and some older people just typing text into a blue screen. Quite boring, but they seemed to enjoy it. Sometimes they even got serious and argued about it. Driven by a toddler’s instinct to imitate older people, I decided to do what the old men were doing. Those guys had a book, “Basic programming for Commodore 64”. I am not very sure about the title, it was plain white with the title in blue. I somehow managed to make a hard copy of the book.

The book contained 50 or so small programs in Basic, and I think an explanation of the code. I am not sure again, because back then I didn’t knew English. My whole vocabulary consisted on (on, off, start, pause, level, game over), the English words you could encounter while dealing with consoles and video games. It was actually 4 years later when I started learning elementary English at school. All I could do was to type the programs and see how it runs. Those kind of programs that draw vertical bars on the screen, a moving disc, a blinking rectangle.. I remember the most “advanced” program had to play from and record to a tape.

As simple the programs were, I was impatiently rushing to type all the code to see the results after execution. I was following the instructions perfectly, and it was amusing. After couple of programs, I started to notice some things. Some of those lines were identical among some programs. Some were different but very similar. I started wondering if I could change the programs a little bit. The first program was obviously a 2 lines Hello world in Basic. I introduced my name instead of Hello, and cautiously run the program. Holy shit! The computer actually displayed my name! That was awesome! I tried to change couple of other places just to get an error in my face. But I noticed something very important, if I change the text between quotes I don’t get an error most of the time. That drew my first separation line between changeable and non changeable code. I ventured more with my programs (changing every quoted text unless I get an error) and some other patterns started to pop up. There were quite few possibilities for the first word in a line (INPUT, PRINT, FOR..), however the rest of the line was often different. Some words showed up only if the line started with a particular word. I kept writing those programs.

I started changing numbers to draw 10 lines instead of 5. Changed some other numbers and noticed sizes and positions of shapes changing on the screen. I think I quickly managed to see every error the Basic interpreter could output, they are not so many anyway. The errors become less and less frequent, and some of them were expected. I started to understand what I was meddling. I was somehow telling the machine what to do. But I was still copying text from the book. So I decided to put the computer solve a personal problem that I had. And I put down the book.

Well, how complicated a 9 years old boy’s problem could be? The problem was: A farmer has a farm of sqm, he buys seeds for bp a kg, and kg of seeds are necessary to plant 1 sqm of land. His crop yields kg/sqm and he sells his harvest for sp a kg. How much mony will the farmer make after selling his crops?

I got to work, I think I spent about 1 week to get a first running version. At the moment I didn’t knew about recording to the tape. My persistent storage was a piece of paper where I type my last advancements. At the end, the program runned and gave correct results! It even could print “The farmer will loose 120″ if revenue-expenses was -120. That was amazing. I think that was the moment I became a programmer. But I didn’t realize it back then.