Deploying a Symfony2 app using Doctrine and PostgreSQL to Heroku

Symfony2 Angular TodoMVC

What is Heroku

Heroku is a platform to quickly and easily deploy your applications to. They abstract much of the sysadmin-type work away so the developer only has to stay at the application-level. If you get everything configured correctly you can end up with a single button click deployment.

image

The free-tier is more than enough to get started and be able to get a sample Symfony2 application up and running with a database backend.

Getting Symfony2 on Heroku

First follow this documentation found on the Heroku site to get a Symfony2 application up and running:

https://devcenter.heroku.com/articles/getting-started-with-symfony2

Making a deploy button

Heroku has documentation on how to do this

https://devcenter.heroku.com/articles/heroku-button

Essentially you create an app.json file that describes the application and tells Heroku how to deploy it.

Using Doctrine with Symfony2 on Heroku

The above documentation gets you to the point where you have a fully functioning Symfony2 application running on Heroku, however it stops there. If you want your application to be database driven and running on the free-tier of Heroku there are a couple more steps you have to take.

Since you install the standard edition in the tutorial, Doctrine is already included as a dependency in your project.

composer.json

...
"require": {
    "php": ">=5.3.3",
    "symfony/symfony": "2.6.x-dev",
    "doctrine/orm": "~2.2,>=2.2.3",
    "doctrine/doctrine-bundle": "~1.2",
    "twig/extensions": "~1.0",
    "symfony/assetic-bundle": "~2.3",
    "symfony/swiftmailer-bundle": "~2.3",
    "symfony/monolog-bundle": "~2.4",
    "sensio/distribution-bundle": "~3.0",
    "sensio/framework-extra-bundle": "~3.0",
    "incenteev/composer-parameter-handler": "~2.0"
},
...

Heroku provides a free instance of a PostgreSQL database so we are going to use that for this example. There also appears to be a MySQL equivalent (ClearDB). Most steps should be able to apply to that too but with replacing PostgreSQL with MySQL.

(Optional) Include PDO_PGSQL as a composer extension dependency

This tells Heroku to enable/install the pdo_pgsql.so extension for your application. This is optional however since pdo_pgsql.so is one of the automatically installed extensions.

composer.json

"require": {
    ...
    "ext-pdo": "*",
    "ext-pdo_pgsql": "*",
    ...
},

Add the PostgreSQL Heroku add-on

After you have deployed your application, use the heroku command to enable the PostgreSQL add-on for your application:

$ heroku addons:add heroku-postgresql:hobby-dev

Add it to your app.json file also to have it automatically enabled for your deploy button

app.json

{
    ...
    "addons": [
        "heroku-postgresql:hobby-dev"
    ]
}

Automatically set your database parameters

When you add the PostgreSQL addon, Heroku automatically provisions a PostgreSQL database for you and provides the access credentials via an environment variable: DATABASE_URL. Unfortunately thats all they provide you, so you have to parse the URL to put it in a format acceptable for Doctrine.

Using a combination of Composer post/pre install commands and some custom code to pull and push variables to the environment, we can get Doctrine properly configured to run correctly when deployed.

Parse and populate the environment variables

I created a simple static callback to grab the DATABASE_URL variable, parse it, and stick other variables back in.

HerokuDatabase.php

<?php

namespace Acme\DemoBundle;

use Composer\Script\Event;

class HerokuDatabase
{
    public static function populateEnvironment(Event $event)
    {
        $url = getenv("DATABASE_URL");

        if ($url) {
            $url = parse_url($url);
            putenv("DATABASE_HOST={$url['host']}");
            putenv("DATABASE_USER={$url['user']}");
            putenv("DATABASE_PASSWORD={$url['pass']}");
            $db = substr($url['path'],1);
            putenv("DATABASE_NAME={$db}");
        }

        $io = $event->getIO();

        $io->write("DATABASE_URL=".getenv("DATABASE_URL"));
    }
}

Update composer.json to run HerokuDatabase::populateEnvironment

Add this as a preinstall step to get the environment populated earlier than the configuration stage

composer.json

"scripts": {
    "pre-install-cmd": [
        "Acme\\DemoBundle\\HerokuDatabase::populateEnvironment"
    ],
    "post-install-cmd": [
        "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile"
    ],
    "post-update-cmd": [
        "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile"
    ]
},

Note: Ensure that you include the buildParameters callback in your post install/update commands

Configure composer-parameter-handler to use the new variables for the database configuration

The heroku provided buildpack for PHP runs composer in the non-interactive mode. This means that the behavior of composer-parameter-handler is to just copy and paste the parameters.yml.dist file to parameters.yml.

Luckily composer-parameter-handler is configurable to automatically replace some parameters with environment variables.

In our case, it would look like:

composer.json

"extra": {
    "incenteev-parameters": {
        "file": "app/config/parameters.yml",
        "env-map": {
            "database_host": "DATABASE_HOST",
            "database_port": "DATABASE_PORT",
            "database_name": "DATABASE_NAME",
            "database_user": "DATABASE_USER",
            "database_password": "DATABASE_PASSWORD"
        }
    }
}

Initialize the database

Almost done, now we just need to initialize the database. Using the heroku command, run the schema creation command:

$ heroku run php app/console doctrine:schema:create

For automation of the deployment button:

app.json

"scripts": {
    "postdeploy": "php app/console doctrine:schema:create"
}

Fin

And you’re done! On the first run of your application it may take a while to load (cache is warming up), however every subsequent page load should be fast :)