Loïc Faugeron Technical Blog

Symfony Differently - part 2: Bootstrap 10/06/2015

This is the second article of the Symfony Differently series, Have a look at the first one: Introduction.

Our goal in this post is to bootstrap an application to then create a search endpoint for items. We've decided to use Symfony for a single reason: our company Acme used it since the begining and the API developers team has Symfony and PHP skills.

Note: Most frameworks have a good community support, a strong documentation, similar features and many developers available on the market. We'll see the importance of a framework performances in the following articles, but the point here is that the choice should mainly rely on skills we have internally (and the ones we can acquire).

Symfony Standard Edition

The first step is to use Composer:

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

This will create a directory structure for our project, download a set of third party libraries and ask us to set a bunch of configuration parameters. In this series we'll use a Postgres database, so we need to edit app/config/config.yml:

# ...

# Doctrine Configuration
doctrine:
    dbal:
        driver: pdo_pgsql

# ...

Once finished we'll save our work using Git:

cd items
git init
git add -A
git ci -m 'Created a standard Symfony application'

Note: We can also use the Symfony Installer to create new projects.

Configuring Apache

For the same reason we chose Symfony we've decided to use Apache for our web servers (it's the one used for all previous projects).

Note: By providing Nginx trainings and recruiting devops used to, it Acme could change its technology stack. The only rule when switching to another technology is to stick to it in order to avoid having too many different technologies.

We need to create a virtual host first by creating the /etc/apache2/sites-available/items.conf file:

<VirtualHost *:80>
    ServerName items.local

    DocumentRoot /home/foobar/items/web

    ErrorLog "/home/foobar/items/app/logs/apache_errors.log"
    CustomLog "/home/foobar/items/app/logs/apache_accesses.log" common

    <Directory /home/foobar/items/web>
        Options Indexes FollowSymLinks MultiViews
        AllowOverride all
        Require all granted
        Order allow,deny
        allow from all
    </Directory>
</VirtualHost>

Apache will require access to the logs and cache directories, as well as our user. The easiest way to avoid permission issues is to change Apache's user and group to ours in /etc/apache2/envvars:

export APACHE_RUN_USER=foobar
export APACHE_RUN_GROUP=foobar

We'll aslo need to add the hostname to our /etc/hosts file:

echo '127.0.0.1 items.local' | sudo tee -a /etc/hosts

Finally we have to enable the website and reload Apache to take the configuration into account:

sudo a2ensite items
sudo service apache2 reload

We now should be able to see the message "Homepage" when browsing http://items.local/app_dev.php/app/example

Tests

A nice tweak to do is to move tests in a separate directory, allowing Composer to only autoload test class in development environments. This can be done by changing composer.json as follow:

{
    "name": "acme/items",
    "license": "private",
    "type": "project",
    "description": "Specific APIs for items",
    "autoload": {
        "psr-4": { "AppBundle\\": "src/AppBundle" }
    },
    "autoload-dev": {
        "psr-4": { "AppBundle\\Tests\\": "tests/" }
    },
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "~2.7@beta",
        "doctrine/orm": "~2.2,>=2.2.3,<2.5",
        "doctrine/dbal": "<2.5",
        "doctrine/doctrine-bundle": "~1.4",
        "symfony/assetic-bundle": "~2.3",
        "symfony/swiftmailer-bundle": "~2.3",
        "symfony/monolog-bundle": "~2.4",
        "sensio/distribution-bundle": "~3.0,>=3.0.12",
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2",
        "incenteev/composer-parameter-handler": "~2.0"
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3"
    },
    "scripts": {
        "post-install-cmd": [
            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
        ],
        "post-update-cmd": [
            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
        ]
    },
    "extra": {
        "symfony-app-dir": "app",
        "symfony-web-dir": "web",
        "symfony-assets-install": "relative",
        "incenteev-parameters": {
            "file": "app/config/parameters.yml"
        }
    }
}

Since we've decided to move vendor binaries back to vendor/bin, we can un-ignore the bin directory by editing .gitignore:

/web/bundles/
/app/bootstrap.php.cache
/app/cache/*
/app/config/parameters.yml
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep
/app/phpunit.xml
/build/
/vendor/
/composer.phar

To make it official, we need to run the following commands:

rm -rf bin
composer update

Note: In production, we'll need to run composer install --no-dev --optimize-autoloader

Our system/functional tests will involve database queries which can make the test suite unreliable. To fix this, we'll create a "middleware" that wraps our AppKernel in a database transaction and rolls it back after each requests:

To do so, we can create the following app/RollbackKernel.php:

<?php
// File: app/RollbackKernel.php

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class RollbackKernel implements HttpKernelInterface
{
    private $app;
    private $connection;

    public function __construct(HttpKernelInterface $app, Connection $connection)
    {
        $this->app = $app;
        $this->connection = $connection;
    }

    public static function make()
    {
        $kernel = new \AppKernel('test', false);
        $kernel->boot();
        $connection = $kernel->getContainer()->get('doctrine.dbal.default_connection');

        return new self($kernel, $connection);
    }

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        $this->connection->beginTransaction();
        $response = $this->app->handle($request, $type, $catch);
        $this->connection->rollback();

        return $response;
    }
}

To be able to use RollbackKernel in our tests we need to make it available by creating the following app/bootstrap_test.php:

<?php
// File: app/bootstrap_test.php

require __DIR__.'/bootstrap.php.cache';
require __DIR__.'/AppKernel.php';
require __DIR__.'/RollBackKernel.php';

Then we can configure PHPUnit to use it by editing app/phpunit.xml.dist:

<?xml version="1.0" encoding="UTF-8"?>

<!-- http://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit backupGlobals="false" colors="true" syntaxCheck="false" bootstrap="bootstrap_test.php">
    <testsuites>
        <testsuite name="Functional Tests">
            <directory>../tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Finally we'll install phpspec with SpecGen for our unit tests:

composer require --dev phpunit/phpunit:~4.6 phpspec/phpspec:~2.2 memio/spec-gen:~0.3
echo 'extensions:' > phpspec.yml
echo '  - Memio\SpecGen\MemioSpecGenExtension' >> phpspec.yml

And now we're ready to test our application! Let's save our work:

git add -A
git commit -m 'Prepared application for tests'

Scripts

There's 3 common tasks we'll be doing as developers with our application:

In order to automate those in a simple way, we've decided to create 3 scripts: bin/build.sh, bin/deploy.sh and bin/test.sh, but for this series we'll only take care of build and test.

The build steps should reinitialize the database and Symfony's cache, for this we'll need Doctrine Fixtures and Doctrine Migrations:

composer require doctrine/doctrine-fixtures-bundle:~2.2
composer require doctrine/migrations:~1.0@alpha
composer require doctrine/doctrine-migrations-bundle:~1.0

Then we have to register them in app/AppKernel.php:

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Symfony\Bundle\AsseticBundle\AsseticBundle(),
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
            new AppBundle\AppBundle(),
        );

        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
            $bundles[] = new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle();
            $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
            $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
            $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
        }

        return $bundles;
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
    }
}

Now we can write the following bin/build.sh script:

#!/usr/bin/env sh

echo ''
echo '// Building development environment'


rm -rf app/cache/* app/logs/*

composer --quiet --no-interaction install --optimize-autoloader > /dev/null

php app/console --quiet doctrine:database:drop --force > /dev/null 2>&1
php app/console --quiet doctrine:database:create
php app/console --no-interaction --quiet doctrine:migrations:migrate
php app/console --no-interaction --quiet doctrine:fixtures:load --fixtures=src

echo ''
echo ' [OK] Development environment built'
echo ''

The test steps should be similar, in addition they will run the test suites and check for coding standards. We can install PHP CS Fixer for this:

composer require --dev fabpot/php-cs-fixer:~1.6

Here's the bin/test.sh script:

#!/usr/bin/env sh

echo ''
echo '// Building test environment'

rm -rf app/cache/test app/logs/*test.log

composer --quiet --no-interaction install --optimize-autoloader  > /dev/null
php app/console --env=test --quiet cache:clear


php app/console --env=test --no-debug --quiet doctrine:database:drop --force > /dev/null 2>&1
php app/console --env=test --no-debug --quiet doctrine:database:create
php app/console --env=test --no-debug --no-interaction --quiet doctrine:migrations:migrate
php app/console --env=test --no-debug --no-interaction --quiet doctrine:fixtures:load --fixtures=src

echo ''
echo ' [OK] Test environment built'
echo ''

vendor/bin/phpunit -c app && vendor/bin/phpspec --no-interaction run --format=dot && vendor/bin/php-cs-fixer fix --dry-run --config=sf23 .

With this we can start the tickets assigned to us, we can commit the changes:

chmod +x bin/*.sh
git add -A
git commit -m 'Created build and test scripts'

Conclusion

Acme's technology stack is composed of Apache2, PostgreSQL, Symfony, PHPUnit and phpspec. In order to ake sure that anyone in the team or any new comers can maintain this new application in the future, we've chosen to stay consistent with the rest.

In the next article, we'll create the search items endpoint in a pragmatic way.