/dev/blog
Bez Hermoso, Software Engineer @ Square
Configuring environment-specific parameters in Symfony2 has been made easy thanks to Incenteev\ParameterHandler\ScriptHandler::buildParameters
. Attaching itself
to the Composer workflow, it provides a very intuitive interface for defining required parameters defined in app/config/parameters.yml.dist
.
In case you don’t know, this mechanism also gives you the ability to specify required parameters specific to your app which developers/deployers need to fill out:
# app/config/parameters.yml.dist
parameters:
database_driver: pdo_mysql
database_host: 127.0.0.1
...
# Project-specific parameters not part of the standard distribution:
elasticsearch_hosts: [ http://localhost:9200 ]
elasticsearch_index: main
A value for elasticsearch_hosts
& elasticssearch_index
will be prompted during composer install
or composer update
, and will default to the specified values if none are provided during the prompt. Really handy.
However, this doesn’t work well with Heroku deploys. Because Heroku runs Composer with --no-interaction
flag,
the prompts are suppressed, and the parameters generated would silently fall back to the default values in the .dist
file.
Work-arounds have been formulated to solve this; the one which is widely adopted being the use of environment variables to set container parameters in conjunction with Symfony’s built-in functionality of converting SYMFONY__*
env variables to container parameters.
Unfortunately, I had some troubles adopting these solutions with the mere fact that some parameters I need are arrays and hashes. Environment variables can only contain scalars like strings and integers.
So I came up with this solution:
Create an app/config/env_parameters.php
file:
<?php
use Symfony\Component\Yaml\Yaml;
/** @var $container \Symfony\Component\DependencyInjection\ContainerBuilder */
$container;
$dist = Yaml::parse(file_get_contents(__DIR__ . '/parameters.yml.dist'));
$distParameters = $dist['parameters'];
foreach ($distParameters as $parameterName => $default) {
if (false !== ($value = getenv('sf2.' . $parameterName))) {
$container->setParameter($parameterName, Yaml::parse($value));
}
}
Yaml::parse
has an unfortunate behavior that can pose a problem here.
See amendments.
Update app/config/config.yml
and add env_parameters.php
as a resource to import:
imports:
- { resource: parameters.yml }
- { resource: env_parameters.php }
...
With this strategy, you can specify Heroku-specific parameters like so:
$ heroku config:set sf2.database_host=us-cdbr-iron-east-01.cleardb.net
$ heroku config:set sf2.database_name=heroku_...
This will also allow for non-scalars like arrays and hashes:
$ heroku config:set sf2.elasticsearch_hosts=[https://....us-east-1.bonsai.io, https://....us-east-1.bonsai.io]
$ heroku config:set sf2.some_config={foo: true, bar: 3.14}
In a nutshell, env_parameters.php
will look at the required parameters defined in parameters.yml.dist
file, and look for
any matching environment variables prefixed with sf2.*
.
If you don’t like mucking around with the composer.json
file to support Heroku deployment,
or you just need to specify non-scalar parameters, give this approach a try.
2014-08-20 Update: Yaml::parse
have an often-unwanted behavior of parsing file contents if the value passed is a valid file-name. A fix is needed in case
one of your parameters are actual filenames:
<?php
use Symfony\Component\Yaml\Yaml;
/** @var $container \Symfony\Component\DependencyInjection\ContainerBuilder */
$container;
$dist = Yaml::parse(file_get_contents(__DIR__ . '/parameters.yml.dist'));
$distParameters = $dist['parameters'];
foreach ($distParameters as $parameterName => $default) {
if (false !== ($value = getenv('sf2.' . $parameterName))) {
$container->setParameter($parameterName, is_file($value) ? $value : Yaml::parse($value));
}
}
2014-08-21 Update: A better, cleaner fix for the unfortunate Yaml::parse
behavior is just instantiating a new Symfony\Component\Yaml\Parser
object:
<?php
use Symfony\Component\Yaml\Parser;
/** @var $container \Symfony\Component\DependencyInjection\ContainerBuilder */
$container;
$parser = new Parser();
$dist = $parser->parse(file_get_contents(__DIR__ . '/parameters.yml.dist'));
$distParameters = $dist['parameters'];
foreach ($distParameters as $parameterName => $default) {
if (false !== ($value = getenv('sf2.' . $parameterName))) {
$container->setParameter($parameterName, $parser->parse($value));
}
}
2014-08-30 Update: David Zuelke (@dzuelke), the developer of heroku/heroku-buildpack-php
,
has pointed out to me that the Heroku PHP buildpack only exports
valid shell identifiers ([A-Z_][A-Z0-9_]*
) during the git push
hook on Heroku. Therefore sf2.*
config variables wouldn’t be accessible
during Composer’s post-install-cmd
hooks.
Luckily, this does not affect the project where I am using this solution on, but it might affect yours if you need to
override parameters that are used during post-install operations that interact with Symfony’s kernel.
In which case, you could use this alternate env_parameters.php
file:
<?php
use Symfony\Component\Yaml\Parser;
/** @var $container \Symfony\Component\DependencyInjection\ContainerBuilder */
$container;
$parser = new Parser();
$dist = $parser->parse(file_get_contents(__DIR__ . '/parameters.yml.dist'));
$distParameters = $dist['parameters'];
foreach ($distParameters as $parameterName => $default) {
/* That's two underscores... */
$envName = 'SF2_' . strtoupper(str_replace('.', '__', $parameterName));
if (false !== ($value = getenv($parameterName))) {
$container->setParameter($parameterName, $parser->parse($value));
}
}
And your config variables should be named like these:
$ heroku config:set SF2_ELASTICSEARCH_HOSTS=[https://....us-east-1.bonsai.io, https://....us-east-1.bonsai.io]
$ heroku config:set SF2_SOME_CONFIG={foo: true, bar: 3.14}
If you need to have parameters with dots in them, like foo.some_entity.class
, substitute dots with two underscores:
$ heroku config:set SF2_FOO__SOME_ENTITY__CLASS=Foo\\SomeBundle\\Entity\\Bar