Symfony2: Avoiding coupling applications to the environment

A quick post looking avoiding tying a Symfony2 application to the environment and instead varying config per environment.

It's tempting to access the env parameter from within an application in order to make decisions. A very crude example designed to make what is happening clear, is getting the environment within from the container in a controller and using that to decide whether to actually send an SMS alert:

if ($this->container->getParameter('kernel.environment') == 'prod') {
    $this->sendSms();
}

We could clean this up by moving the sending of the SMS into a service and injecting the value of the parameter into it:

namespace Acme\DemoBundle\Sms\Sender;

class Sender
{
    private $env

    public function __construct($env)
    {
        $this->env = $env;
    }
    
    public function send($number, $message)
    {
        if($this->env != 'prod') {
            return;
        }
        // ...
    }
}

with this service definition:

<!-- src/Acme/DemoBundle/Resources/config/services.xml-->
<?xml version="1.0" ?>

<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="acme.sms_sender" 
                  class="Acme\DemoBundle\Sms\Sender">
             <argument>%kernel.environment%</argument>
         </service>
     </services>
</container>

However whilst moving the sending of the SMS out of the controller is a good idea we are still coupling the service to the environment.

Why this is bad in theory

The application should not be aware of what environment it is running under, it should just be configured a particular way based on its configuration files. Since we want to vary configuration between different environments there are typically different configuration for each of these environments. We choose which of these sets of configuration to load rather than telling the application which environment it is in.

Why this is bad in practice

This is all very well in theory but what problem's can it cause us to make the application aware of what environment it is in in practice? The first is that we can only change the configuration now in the code if we change our mind about the setting. The second is that we can no longer test the setting by changing config, we need to change the environment the application is running in altogether to see the effect, if we used a configuration value instead we could just change that for the current environment.

If you later need to add further environments which need the same change then you will need to add to the if statements in the code to do this. With a configuration value you can just set it for that environment if it needs a value different to the default configuration.

With a good parameter name it is then clear as to its purpose as well, so reconfiguring the application is straightforward without having to dig into the application code to see what the parameter is used for.

namespace Acme\DemoBundle\Sms\Sender;

class SmsSender
{
    private $sendingDisabled

    public function __construct($sendingDisabled)
    {
        $this->sendingDisabled = $sendingDisabled;
    }
    
    public function send($number, $message)
    {
        if($this->sendingDisabled) {
            return;
        }
        // ...
    }
}

with this service definition:

<!-- src/Acme/DemoBundle/Resources/config/services.xml-->
<?xml version="1.0" ?>

<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="acme.sms_sender" 
                  class="Acme\DemoBundle\Sms\Sender">
             <argument>%acme.sms.disabled%</argument>
         </service>
     </services>
</container>

and in the config_dev.yml file:

parameters:
    acme.sms.disabled: true

Rather than simply setting this as a parameter you could expose it as bundle configuration as I looked at in my previous post.