Explore! Environments & Config Files
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNot unlike our space-traveling users, we are also pretty adventurous. Sure, they might be discovering intelligent life on other planets or exploring binary planets inside the habitable zone. But we! We are going to explore the config/
directory and learn all of its secrets. Seriously, this is cool stuff!
Environment?
We know that Symfony is really just a set of routes and a set of services. And we also know that the files in config/packages
configure those services. But, who loads these files? And what is the importance - if any - of these sub-directories?
Well, put on your exploring pants, because we're going on a journey!
The code that runs our app is like a machine: it shows articles and will eventually allow people to login, comment and more. The machine always does the same work, but... it needs some configuration in order to do its job. Like, where to write log files or what the database name and password are.
And there's other config too, like whether to log all messages or just errors, or whether to show a big beautiful exception page - which is great for development - or something aimed at your end-users. Yep, the behavior of your app can change based on its config.
Symfony has an awesome way of handling this called environments. It has two environments out-of-the-box: dev
and prod
. In the dev
environment, Symfony uses a set of config that's... well... great for development: big errors, log everything and show me the web debug toolbar. The prod
environment uses a set of config that's optimized for speed, only logs errors, and hides technical info on error pages.
How Environments Work
Ok, I know what you're thinking: this makes sense from a high level... but how does it work? Show me the code!
Open the public/
directory and then index.php
:
// ... lines 1 - 2 | |
use App\Kernel; | |
use Symfony\Component\Debug\Debug; | |
use Symfony\Component\Dotenv\Dotenv; | |
use Symfony\Component\HttpFoundation\Request; | |
require __DIR__.'/../vendor/autoload.php'; | |
// The check is to ensure we don't use .env in production | |
if (!isset($_SERVER['APP_ENV'])) { | |
if (!class_exists(Dotenv::class)) { | |
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.'); | |
} | |
(new Dotenv())->load(__DIR__.'/../.env'); | |
} | |
$env = $_SERVER['APP_ENV'] ?? 'dev'; | |
$debug = $_SERVER['APP_DEBUG'] ?? ('prod' !== $env); | |
if ($debug) { | |
umask(0000); | |
Debug::enable(); | |
} | |
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { | |
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); | |
} | |
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { | |
Request::setTrustedHosts(explode(',', $trustedHosts)); | |
} | |
$kernel = new Kernel($env, $debug); | |
$request = Request::createFromGlobals(); | |
$response = $kernel->handle($request); | |
$response->send(); | |
$kernel->terminate($request, $response); |
This is the front controller: a fancy word to mean that it is the first file that's executed for every page. You don't normally worry about it, but... it's kind of interesting.
It's looking for an environment variable called APP_ENV
:
// ... lines 1 - 9 | |
// The check is to ensure we don't use .env in production | |
if (!isset($_SERVER['APP_ENV'])) { | |
// ... lines 12 - 15 | |
} | |
// ... lines 17 - 40 |
Tip
If you start a new project today, you won't see this APP_ENV
logic. It's
been moved to a config/bootstrap.php
file.
We're going to talk more about environment variables later, but they're just a way to store config values. One confusing thing is that environment variables are a totally different thing than what we're talking about right now: Symfony environments.
Forget how the $env
variable is set for a moment, and go down to see how it's used:
// ... lines 1 - 17 | |
$env = $_SERVER['APP_ENV'] ?? 'dev'; | |
// ... lines 19 - 34 | |
$kernel = new Kernel($env, $debug); | |
// ... lines 36 - 40 |
Ah! It's passed into some Kernel
class! The APP_ENV
variable is set in a .env
file, and right now it's set to dev
. Again, more on environment variables later.
Anyways, the string dev
- is being passed into a Kernel
class. The question is... what does that do?
Debugging the Kernel Class
Well... good news! That Kernel
class is not some core part of Symfony. Nope, it lives right inside our app! Open src/Kernel.php
:
// ... lines 1 - 2 | |
namespace App; | |
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; | |
use Symfony\Component\Config\Loader\LoaderInterface; | |
use Symfony\Component\DependencyInjection\ContainerBuilder; | |
use Symfony\Component\HttpKernel\Kernel as BaseKernel; | |
use Symfony\Component\Routing\RouteCollectionBuilder; | |
class Kernel extends BaseKernel | |
{ | |
use MicroKernelTrait; | |
const CONFIG_EXTS = '.{php,xml,yaml,yml}'; | |
public function getCacheDir() | |
{ | |
// ... line 19 | |
} | |
public function getLogDir() | |
{ | |
// ... line 24 | |
} | |
public function registerBundles() | |
{ | |
// ... lines 29 - 34 | |
} | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
// ... lines 39 - 47 | |
} | |
protected function configureRoutes(RouteCollectionBuilder $routes) | |
{ | |
// ... lines 52 - 59 | |
} | |
} |
After some configuration, there are three methods I want to look at. The first is registerBundles()
:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 26 | |
public function registerBundles() | |
{ | |
$contents = require $this->getProjectDir().'/config/bundles.php'; | |
foreach ($contents as $class => $envs) { | |
if (isset($envs['all']) || isset($envs[$this->environment])) { | |
yield new $class(); | |
} | |
} | |
} | |
// ... lines 36 - 60 | |
} |
This is what loads the config/bundles.php
file:
// ... lines 1 - 2 | |
return [ | |
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], | |
Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], | |
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], | |
Symfony\Bundle\TwigBundle\TwigBundle::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], | |
Knp\Bundle\MarkdownBundle\KnpMarkdownBundle::class => ['all' => true], | |
]; |
And check this out: some of the bundles are only loaded in specific environments. Like, the WebServerBundle
is only loaded in the dev
environment:
// ... lines 1 - 2 | |
return [ | |
// ... line 4 | |
Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], | |
// ... lines 6 - 11 | |
]; |
And the DebugBundle is similar. Most are loaded in all
environments.
The code in Kernel
handles this: you can pretty easily guess that $this->environment
is set to the environment, so, dev
!
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 26 | |
public function registerBundles() | |
{ | |
$contents = require $this->getProjectDir().'/config/bundles.php'; | |
foreach ($contents as $class => $envs) { | |
if (isset($envs['all']) || isset($envs[$this->environment])) { | |
// ... line 32 | |
} | |
} | |
} | |
// ... lines 36 - 60 | |
} |
The other two important methods are configureContainer()
... which basically means "configure services"... and configureRoutes()
:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 36 | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
$container->setParameter('container.autowiring.strict_mode', true); | |
$container->setParameter('container.dumper.inline_class_loader', true); | |
$confDir = $this->getProjectDir().'/config'; | |
$loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob'); | |
if (is_dir($confDir.'/packages/'.$this->environment)) { | |
$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) | |
{ | |
$confDir = $this->getProjectDir().'/config'; | |
if (is_dir($confDir.'/routes/')) { | |
$routes->import($confDir.'/routes/*'.self::CONFIG_EXTS, '/', 'glob'); | |
} | |
if (is_dir($confDir.'/routes/'.$this->environment)) { | |
$routes->import($confDir.'/routes/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); | |
} | |
$routes->import($confDir.'/routes'.self::CONFIG_EXTS, '/', 'glob'); | |
} | |
} |
Of course! Because - say it with me now:
Symfony is just a set of services and routes.
Ok, I'll stop jamming that point down your throat.
Package File Loading
Look at configureContainer()
first:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 36 | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
$container->setParameter('container.autowiring.strict_mode', true); | |
$container->setParameter('container.dumper.inline_class_loader', true); | |
$confDir = $this->getProjectDir().'/config'; | |
$loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob'); | |
if (is_dir($confDir.'/packages/'.$this->environment)) { | |
$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 49 - 60 | |
} |
When Symfony boots, it needs config: it needs to know where to log or how to connect to the database. To get all of the config, it calls this one method. You can ignore these first two lines: they're internal optimizations.
After, it's uses some sort of $loader
to load configuration files:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 36 | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
// ... lines 39 - 41 | |
$loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob'); | |
if (is_dir($confDir.'/packages/'.$this->environment)) { | |
$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 49 - 60 | |
} |
This CONFIG_EXTS
constant is just a fancy way to load any PHP, XML or YAML files:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 14 | |
const CONFIG_EXTS = '.{php,xml,yaml,yml}'; | |
// ... lines 16 - 60 | |
} |
First, it loads any files that live directly in packages/
:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 36 | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
// ... lines 39 - 41 | |
$loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob'); | |
// ... lines 43 - 47 | |
} | |
// ... lines 49 - 60 | |
} |
But then, it looks to see if there is an environment-specific sub-directory, like packages/dev
. And if there is, it loads all of those files:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 36 | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
// ... lines 39 - 42 | |
if (is_dir($confDir.'/packages/'.$this->environment)) { | |
$loader->load($confDir.'/packages/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); | |
} | |
// ... lines 46 - 47 | |
} | |
// ... lines 49 - 60 | |
} |
Right now, in the dev
environment, it will load 5 additional files. The order of how this happens is the key: any overlapping config in the environment-specific files override those from the main files in packages/
.
For example, open the main routing.yaml
. This is not very important, but it sets some strict_requirements
flag to ~
... which is null
in YAML:
framework: | |
router: | |
strict_requirements: ~ |
But then in the dev
environment, that's overridden: strict_requirements
is set to true
:
framework: | |
router: | |
strict_requirements: true |
To prove it, find your terminal and run:
./bin/console debug:config framework
Since we're in the dev
environment right now... yep! The strict_requirements
value is true
!
This also highlights something we talked about earlier: the names of the files are not important... at all. This could be called hal9000.yaml
and not change a thing. The important part is the root key, which tells Symfony which bundle is being configured.
Usually, the filename matches the root key... ya know for sanity. But, it doesn't have to. The organization of these files is subjective: it's meant to make as much sense as possible. The routing.yaml
file actually configures something under the framework
key.
My big point is this: all of these files are really part of the same configuration system and, technically, their contents could be copied into one giant file called my_big_old_config_file.yaml
.
Oh and I said earlier that Symfony comes with only two environments: dev
and prod
. Well... I lied: there is also a test
environment used for automated testing. And... you can create more!
Go back to Kernel.php
. The last file that's loaded is services.yaml
:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 36 | |
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) | |
{ | |
// ... lines 39 - 45 | |
$loader->load($confDir.'/services'.self::CONFIG_EXTS, 'glob'); | |
$loader->load($confDir.'/services_'.$this->environment.self::CONFIG_EXTS, 'glob'); | |
} | |
// ... lines 49 - 60 | |
} |
More on that file later. It can also have an environment-specific version, like services_test.yaml
.
Route Loading
And the configureRoutes()
method is pretty much the same: it automatically loads everything from the config/routes
directory and then looks for an environment-specific subdirectory:
// ... lines 1 - 10 | |
class Kernel extends BaseKernel | |
{ | |
// ... lines 13 - 49 | |
protected function configureRoutes(RouteCollectionBuilder $routes) | |
{ | |
$confDir = $this->getProjectDir().'/config'; | |
if (is_dir($confDir.'/routes/')) { | |
$routes->import($confDir.'/routes/*'.self::CONFIG_EXTS, '/', 'glob'); | |
} | |
if (is_dir($confDir.'/routes/'.$this->environment)) { | |
$routes->import($confDir.'/routes/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); | |
} | |
$routes->import($confDir.'/routes'.self::CONFIG_EXTS, '/', 'glob'); | |
} | |
} |
So.. yea! All of the files inside config/
either configure services or configure routes. No biggie.
But now, with our new-found knowledge, let's tweak the cache service to behave differently in the dev
environment. And, let's learn how to change to the prod
environment.
I think that it's more intuitive if the boot function was outside the handle function
<br />$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);<br />$kernel->boot();<br />$request = Request::createFromGlobals();<br />$response = $kernel->handle($request);<br />$response->send();<br />$kernel->terminate($request, $response);<br />
So the handle request function only captures the request