Easier Symfony debugging across environments

If only code was as easy to debug this. You can try, but you'll just end up with a wet computer.

One of the odd quirks of Symfony since version 1 has been it's debug mode. While it's name may have changed from frontend_dev.php to app_dev.php, it's always been the same idea, to run in debug mode with your dev configuration.

What if you don't just want to debug your dev configuration though? Most companies, and even solo developers, have multiple environments and if you're using third-party providers for data, you're likely to have different configuration options for each environment.

As long as your debug request cannot be reached by external users (and isn't cached - if you forget this once, you're not going to forget it again) then it should be fine to use. If you pair this with custom data collectors for the web profiler then you'll find plenty of reasons of why having access to a debug mode on your environments is a useful idea.

How do we do it then? We're going to alter a few files in an existing Symfony 3 install. We're also going to need some way of setting the environment that's external to Symfony. I'd recommend using an environment variable as Symfony 3.2 has some nice way to interact with them and it's very Docker-friendly. Let's call the environment variable SYMFONY_ENV.

The app_dev.php

Let's start with the web/app_dev.php file then. You can find the original code for this on Github. First up, let's rename it to web/app_debug.php as we're not just about the dev environment any more.

We're going to strip out the ip-address checks as we're going to do that in Apache instead, and then we're going to replace the AppKernel line so that it's not forced to the dev environment.

// web/app_debug.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Debug\Debug;

/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__.'/../app/autoload.php';
Debug::enable();
$kernel = new AppKernel(getenv('SYMFONY_ENV') ?: 'prod', true);
...

The AppKernel.php

If you leave it at the above change then you'll have the basics of what you need, but there are some other changes that make this a lot more useful. for that we're going to want to alter the app/AppKernel.php.

Looking at the source code stored in Github we can see that some of the bundles are registered based on the environment. That's not what we really want as some of those are obviously debug-only, not just dev and test. Let's fix this then.

Replace the lines that look like:

// app/AppKernel.php
...

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

With the following code. Instead of assuming that dev and test environments are automatically for debugging, we can now choose to use the debugger and web profiler:

// app/AppKernel.php
...

if ($this->isDebug()) {
    $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
    $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
    $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
}

if ($this->getEnvironment() === 'dev') {
    $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
...

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

You can also see that we've added some code to the end of the file. This allows us to have a separate config for debug mode. This is required as we've now split out some of the bundles to be debug-only, which gives us the choice of either making sure our config for these bundles is always loaded, or having a separate config file.

Although we're splitting our code up so we don't have some environments being special, I've left one of the bundles as being dev-only. This is because the SensioGeneratorBundle, to use the official description, "Generates Symfony bundles, entities, forms, CRUD, and more...". This is the type of bundle that only makes sense for a developer to use during development so shouldn't be accessible to any other environment.

The config_debug.yml

The separate config file will take bits of the config_dev.yml file and relocate them to load only when debug is active. Create a new file called app/config/config_debug.yml and add the following lines:

# app/config/config_debug.yml
web_profiler:
    toolbar: "%kernel.debug%"
    intercept_redirects: false

framework:
    router:
        resource: "%kernel.root_dir%/config/routing_debug.yml"
        strict_requirements: true
        profiler: {
            only_exceptions: false
        }

monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: [!event]
        console:
            type: console
            channels: [!event, !doctrine]

        # uncomment to get logging in your browser
        # you may have to allow bigger header sizes in your Web server configuration
        #firephp:
        #    type: firephp
        #    level: info
        #chromephp:
        #    type: chromephp
        #    level: info

Note the change in the routing url above to routing_debug.yml. We're going to come to that next.

Now go into app/config/config_dev.yml and remove the above sections from here. Symfony won't fail if you leave them here but there's a good chance you may get confused in future if you leave them alone.

The routing_debug.yml

Symfony, by default, comes with a routing_dev.yml which handles additional routes the web profiler. This will need to change as we need them on debug, not on simply being in the development environment. For this then, we're going to do the super-complex task of renaming app/config/routing_dev.yml to app/config/routing_debug.yml.

That's it. No really, that's it. If you clear the cache and test it, everything should be working and you can have your environments separate to your debugging.

The console

So you've followed the instructions letter for letter right? Nope?! Your company has an environment variable set already that's not SYMFONY_ENV and you want to use that? You've altered all the times I've used SYMFONY_ENV but your Symfony commands fail? There's a reason for that. There is exactly one time that Symfony uses a preset environment variable and that's in bin/console.

See? Only 1 and it's in the console code. You don't just have to take my word for it!

What you'll need to do is change the following line in your bin/console to match your environment variable:

// bin/console

...
$env = $input->getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev');


And then everything should work nicely again.

Security

This may be at the end of the post, but it's a very important point. If you don't secure your app_debug.php correctly, you will be exposing vital information to the outside world. Stuff you really don't want anyone to see like your secret Nickelback fan songs.

There are a few ways to secure this. The simplest way is don't deploy app_debug.php to externally-viewable environments. This will remove some of the benefit of having an app_debug.php, but it will prevent any unexpected leakage.

If you're always on a company VPN, or have a static IP range, or use a jumpbox to reach your servers, then you have the option to block access by IP address. The following is a snippet from our apache vhost file. The names have been changed to protect the innocent.

SetEnvIf X-Forwarded-For ^101\.101\. is_through_proxy
<LocationMatch ^.*_debug.php.*>
    <If "env('SYMFONY_ENV') == 'dev'">
        Allow from all
    </If>
    <Else>
        Order Deny,Allow
        Deny from all
        Allow from env=is_through_proxy
        Allow from 101.101.0.0/255.255.0.0
    </Else>
</LocationMatch>


The above always allows access for the dev environment, and then limits the rest by ip address. The SetEnvIf line picks up when the site's been accessed by being forwarded through our jumpbox and then uses a simple Allow from env check to allow access if that's occurred.

Lastly, you can simply add basic auth to your app_debug.php path. Make sure that it also protects all paths beginning with app_debug.php/ or you'll have a large security hole with your profiler.