Managing Application Configuration
Most applications have a few custom bits of configuration. For instance, you might configure your error handler, or add some special facebook authentication key. Generally, these fall into two categories:
- configuration specific to the application (how errors are handled)
- configuration specific to the environment (which key to use for a service in staging/prod)
For the former, I like creating a directory structure similar to the following:
$ ls config/
app.php
bootstrap.php
bootstrap_cli.php
bootstrap/environment.php
bootstrap/functions.php
bootstrap/functions_cli.php
bootstrap/keys.php
bootstrap/services.php
paths.php
routes.php
Generally speaking, I have a bootstrap
folder which contains multiple php files I require from my bootstrap.php
. I use the _cli
suffix on the filename to denote cli-based configuration. I also separate the config by the type of thing I am configuring, e.g. keys.php
contains keys for stuff like an S3 bucket, while services.php
contains a list of services mapping to their tcp
or udp
urls.
Sometimes I don’t want to store this information in the repository. For instance, I might have a specific bit of authentication information for the Facebook application my app is communicating with, or credentials to some SFTP bucket where important documents are stored. Maybe the database credentials are sacred and I don’t want everyone on the dev team to connect directly to production. Generally speaking, I have alternatives I would use in this case so that the functionality works both locally and in production, albeit with slightly different data.
In this case, I use php-dotenv
to configure environment variables for use in my application. Let’s install it in our application first:
composer require josegonzalez/dotenv
Normally I add the following bit of code right after the composer vendor/autoload.php
is required in my config/bootstrap.php
. This will affect both cli and web requests, so there isn’t a need to do it twice:
if (!env('APP_NAME')) {
josegonzalez\Dotenv\Loader::load([
'filepaths' => [
__DIR__ . DS . '.env',
__DIR__ . DS . '.env.default',
],
'toServer' => false,
'skipExisting' => ['toServer'],
'raiseExceptions' => false
]);
}
A few things:
- The php-dotenv project supports being called in a non-static way if you hate statics.
.env
files are simply a list ofexport KEY=VALUE
statements. If you know bash, you know how to use.env
files. There is a primer in the readme.- You can load multiple
.env
files. The first one that exists on disk will be used. This is useful if you havegitignored
one like I do but wish to provide a default.env
file - You can tell
php-dotenv
to populate a number of variables. In this case, I am populating$_SERVER
. - By default, exceptions are raised whenever there is an issue loading or parsing a
.env
file. Rather than raise an expection at the bootstrap level, I just turn them off and assume the application has sane defaults. YMMV.
Now, when is this useful? Say I have a default database config, and I store this in my config/.env.default
:
# cakephp can read DSNs, remember?
export DATABASE_URL="mysql://user:password@localhost/database?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false"
And I read it into my config/app.php
like so:
'Datasources' => [
'default' => [
'url' => env('DATABASE_URL'),
],
],
In production, my nginx.conf
sets APP_NAME
and DATABASE_URL
, and therefore I don’t load the default mysql configuration. But what if I didn’t? I could create a config/.env
file on my server with the following:
export DATABASE_URL="mysql://app:pass@some-host/app-database?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=true"
And my application would be none the wiser. What’s even more awesome is that I can also use this same trick to provide custom environments locally. If I have a developer who has slightly different config than the defaults, they can simply create a config/.env
file with their own customizations and they are off to the races!