Environments

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Your app - the PHP code you write - is a machine: it does whatever interesting thing you told it to do. But that doesn't mean your machine always has the same behavior: by giving that machine different config, you can make it work in different ways. For example, during development, you probably want your app to display errors and your logger to log all messages. But on production, you'll probably want to pass configuration to your app that tells it to hide exception messages and to only write errors to your log file.

To help with this, Symfony has a powerful concept called "environments". This has nothing to do with server environments - like your "production environment" or "staging environment". In Symfony, an environment is a set of configuration. And by default, there are two environments: dev - the set of config that logs everything and shows the big exception page - and prod, which is optimized for speed and hides error messages.

And we can see these environments in action! Open up public/index.php:

28 lines public/index.php
... lines 1 - 2
use App\Kernel;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/config/bootstrap.php';
if ($_SERVER['APP_DEBUG']) {
umask(0000);
Debug::enable();
}
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
}
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
Request::setTrustedHosts([$trustedHosts]);
}
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

This is your "front controller": a fancy way of saying that it's the file that's always executed first by your web server.

Where the Environment String is Set

If you scroll down a bit - most things aren't too important - this eventually instantiates an object called Kernel and passes it $_SERVER['APP_ENV']:

28 lines public/index.php
... lines 1 - 22
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
... lines 24 - 28

That APP_ENV thing is configured in another file - .env - at the root of your project:

22 lines .env
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=c28f3d37eba278748f3c0427b313e86a
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'
###

There it is: APP_ENV=dev:

22 lines .env
... lines 1 - 15
###> symfony/framework-bundle ###
APP_ENV=dev
... lines 18 - 22

So right now, we are running our app in the dev environment. By the way, this entire file is a way to define environment variables. Despite the similar name, environment variables are a different concept than Symfony environments... and we'll talk about them later.

Right now, the important thing to understand is that when this Kernel class is instantiated, we're currently passing the string dev as its first argument:

28 lines public/index.php
... lines 1 - 22
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
... lines 24 - 28

If you want to execute your app in the prod environment, you would change the value in .env:

22 lines .env
... lines 1 - 15
###> symfony/framework-bundle ###
APP_ENV=dev
... lines 18 - 22

We'll do exactly that in a few minutes.

Kernel: How Environments Affect things

Anyways, this Kernel class is actually not some core class hiding deep in Symfony. Nope! It lives in our app: src/Kernel.php. Open that up:

55 lines src/Kernel.php
... lines 1 - 2
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
private const CONFIG_EXTS = '.{php,xml,yaml,yml}';
public function registerBundles(): iterable
{
$contents = require $this->getProjectDir().'/config/bundles.php';
foreach ($contents as $class => $envs) {
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
yield new $class();
}
}
}
public function getProjectDir(): string
{
return \dirname(__DIR__);
}
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
$container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
$container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug);
$container->setParameter('container.dumper.inline_factories', true);
$confDir = $this->getProjectDir().'/config';
$loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
}
protected function configureRoutes(RouteCollectionBuilder $routes): void
{
$confDir = $this->getProjectDir().'/config';
$routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
}
}

The Kernel is the heart of your application. Well, you won't need to look at it often... or write code in it... maybe ever, but it is responsible for initializing and tying everything together.

What does that mean? You can kind of think of a Symfony app as just 3 parts. First, Symfony needs to know what bundles are in the app. That's the job of registerBundles():

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 17
public function registerBundles(): iterable
{
$contents = require $this->getProjectDir().'/config/bundles.php';
foreach ($contents as $class => $envs) {
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
yield new $class();
}
}
}
... lines 27 - 53
}

Then, it needs to know what config to pass to those bundles to help them configure their services. That's the job of configureContainer():

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 32
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
$container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
$container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug);
$container->setParameter('container.dumper.inline_factories', true);
$confDir = $this->getProjectDir().'/config';
$loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
}
... lines 45 - 53
}

And finally, it needs to get a list of all the routes in your app. That's the job of configureRoutes():

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 45
protected function configureRoutes(RouteCollectionBuilder $routes): void
{
$confDir = $this->getProjectDir().'/config';
$routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
}
}

By the way, if you start a Symfony 5.1 app, you probably won't see a registerBundles() method. That's because it was moved into a core trait, but it has the exact logic that you see here.

registerBundles()

Back up in registerBundles(), the flag that we passed to Kernel - the dev string - eventually becomes the property $this->environment:

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 17
public function registerBundles(): iterable
{
... line 20
foreach ($contents as $class => $envs) {
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
... line 23
}
}
}
... lines 27 - 53
}

This methods uses that. Open up config/bundles.php:

... lines 1 - 2
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Knp\Bundle\MarkdownBundle\KnpMarkdownBundle::class => ['all' => true],
];

Notice that all of the bundles classes are set to an array, like 'all' => true or some have 'dev' => true and 'test' => true. This is declaring which environments that bundle should be enabled in. Most bundles will be enabled in all environments. But some - like DebugBundle or WebProfilerBundle - are tools for development. And so, they are only enabled in the dev environment. Oh, and there is also a third environment called test, which is used if you write automated tests.

Over in registerBundles(), this loops over the bundles and uses that info to figure out if that bundle should be enabled in the current environment or not:

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 17
public function registerBundles(): iterable
{
... line 20
foreach ($contents as $class => $envs) {
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
yield new $class();
}
}
}
... lines 27 - 53
}

This is why the web debug toolbar & profiler won't show up in the prod environment: the bundle that powers those isn't enabled in prod!

configureContainer: Environment-Specific Config Files

Anyways, bundles give us services and, as we've learned, we need the ability to pass config to those bundles to control those services. That's the job of configureContainer():

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 32
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
$container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
$container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug);
$container->setParameter('container.dumper.inline_factories', true);
$confDir = $this->getProjectDir().'/config';
$loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
}
... lines 45 - 53
}

I love this method. It's completely responsible for loading all the config files inside the config/ directory. Skip passed the first 4 lines, if you have them, which set a few low-level flags.

The real magic is this $loader->load() stuff, which in a Symfony 5.1 app will look like $container->import()... but it works the same. This code does one simple thing: loads config files. The first line loads all files in the config/packages/ directory. That self::CONFIG_EXTS thing refers to a constant that tells Symfony to load any files ending in .php, .xml, .yaml, .yml. Most people use YAML config, but you can also use XML or PHP.

Anyways, this is the line that loads all the YAML files inside config/packages. I mentioned earlier that the names of these files aren't important. For example, this file is called cache.yaml even though it's technically configuring the framework bundle:

framework:
cache:
... lines 3 - 20

This shows why:

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 32
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
... lines 35 - 39
$loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
}
... lines 45 - 53
}

Symfony loads all of the files - regardless of their name - and internally creates one giant, array of configuration. Heck, we could combine all the YAML files into one big file and everything would work fine.

But what I really want you to see is the next line. This says: load everything from the config/packages/ "environment" directory:

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 32
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
... lines 35 - 40
$loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
... lines 42 - 43
}
... lines 45 - 53
}

Because we're in the dev environment, it's loading the 4 files in config/packages/dev. This allows us to override configuration in specific environments!

For example, in the prod/ directory, open the routing.yaml file. This configures the router and sets a strict_requirements key to null:

framework:
router:
strict_requirements: null

It's not really important what this does. What is important is that the default value for this is true, but a better value for production is null. This override accomplishes that. I'll close that file.

So this whole idea of environments is, ultimately, nothing more than a configuration trick: Symfony loads everything from config/packages and then loads the files in the environment subdirectory... which lets us override the original values.

Oh, these last two lines load services.yaml and services_{environment}.yaml:

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 32
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
... lines 35 - 41
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
}
... lines 45 - 53
}

That's where we add our own services to the container and we'll talk about them soon.

configureRoutes()

Ok, we've now initialized our bundles and loaded config. The last job of Kernel is to figure out what routes our app needs. Look down at configureRoutes():

55 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel
{
... lines 14 - 45
protected function configureRoutes(RouteCollectionBuilder $routes): void
{
$confDir = $this->getProjectDir().'/config';
$routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
}
}

Ah, it does... pretty much the exact same thing as configureContainer(): it loads all the files from config/routes - which is just one annotations.yaml file - and then loads any extra files in config/routes/{environment}.

Let's look at one of these: config/routes/dev/web_profiler.yaml:

web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

This is what's responsible for importing the web debug toolbar and profiler routes into our app! At your terminal, run:

php bin/console debug:router

Yep! These /_wdt and /_profiler routes are here thanks to that file. This is another reason why the web debug toolbar & profiler won't be available in the prod environment.

Next, let's change environments: from dev to prod and see the difference. We're also going to use our new environment knowledge to change the cache configuration only in the prod environment.

Leave a comment!