I presented a talk at CakeFest 2015 about CakePHP configuration by extracting the differences in your app that are unique to each operating environment out into config files and I wanted to sum up the approach here. There is a repo available with sample CakePHP 3 / CakePHP 2 apps and well-annotated slides.
Overview
To start, I need to define what I mean by operating environments. It’s likely that you’re already familiar with them if you develop on your laptop or desktop but host your webapp on a hosting provider’s server. These are operating environments, and your app usually has to treat something about each one differently. If nothing else, the database connection your Cake app uses is probably different for your local development copy than it is in your publicly accessible “live” copy of the site.
Some Common Approaches
There are a lot of ways of dealing with this fact, but they all get complicated by source control systems like git or svn and especially when you have multiple developers working on a project. Everyone needs to adapt the same Cake app to connect to their own database server. One possible option is to somehow detect the environment in your code wherever you need to do something different and switch on that inline in your codebase. Take this layout/default.ctp
snippet, for example:
// src/Template/Layout/default.ctp
<?php
if ($_SERVER['SERVER_NAME'] === 'www.site.com') {
echo 'This is production';
} elseif ($_SERVER['SERVER_NAME'] === 'stage.site.com') {
echo 'This is staging';
} else {
echo 'This is development';
}
?>
This sorta gets the job done, but you’d have to repeat that check everywhere you needed a different config per-environment, and this method also hard-codes the possible hostnames directly into the code. If you ever need to change URLs, you’ll have a lot of updating to do. Overall, a pretty crude choice.
A second approach is to move those environment-specific values into a config file, but not store the config in the code repository at all. This will force each user or hosting environment to write their own version of the config file, but this is error prone and makes it difficult to track what the settings are as they are used in each environment. Again a bad option.
As a third option, we could store a uniquely named config file in source control for each environment, and then move them into place during your deployment process. This has the advantage that all configs are tracked, but might also be a double-edged sword if you’re storing sensitive keys or passwords in the repo. This may work in simple cases, but I think we can do better. First, let’s examine what a “good” system would do for us.
Concepts
We should isolate where we detect the current environment down to a single spot in our code base. Once we have an environment, the app should never need to “switch” to different configs again.
The value that defines what environment we’re in should be artificial. We want to define our environment, not inherit it. That means we should set an environment variable or use a single file, and not depend on the hostname used to access the app, or the server’s IP address (either of which may need to change unexpectedly.)
Every config value that isn’t “sensitive” should be tracked in source control, so that developers can update them for every environment and reference them without having to log into a specific server.
Like CakePHP itself, we should favor convention over configuration, so we should define a single “baseline” set of configs, and then only override values as necessary for other environments.
Any settings that are different for an environment should only be loaded once. As mentioned before, we want any switching to happen in a single spot, and from then on the Cake app should run with whatever set of values we provide to it. Additionally, the sum of all config keys must be available in every environment, albeit with a different value. This ensures that our app can always read a key.
Keys should only be overridden when necessary. If a second environment uses the same specific config value as the first, then the second should “inherit” the first’s value. This helps keep the configs DRY so we don’t have to update the same value in multiple configs unnecessarily.
We should recognize and accept the case that sometimes we shouldn’t track a config value in the repository. Maybe the credentials are too sensitive, or maybe a developer needs to override something locally and we don’t want them to accidentally commit that setting in a tracked file. In either case, a competent system will provide a mechanism for this.
At the end of the day, the app should actually be ignorant of what environment it’s running in. Once the other principles are met, the app will always have a complete set of config keys to read, and can act on them the same way, just using their different values. More specifically, we never want our code to have to reference the “name” of any particular environment directly. This will be easier to demonstrate with some examples.
Making it Happen
My proposal for meeting these needs is to leverage Cake’s existing Configure
class. It’s universally available in any Cake app, and already provides great support for loading multiple configs and merging them together. Even better, Cake 3 unifies a lot of previously-disparate configs for your Database and Email into Configure, which does most of the work for us.
The place we need to start is by defining a standard environment flag. I’m going to use an Apache SetEnv
directive and a command line environment variable:
# my_apache_vhost.conf
<VirtualHost *:80>
ServerName stagingsite.com
SetEnv APP_ENV staging
</VirtualHost>
# ~/.profile or ~/.bash_profile
export APP_ENV=staging
Then we’re going to enhance our config/bootstrap.php
file to look for new files based on the value of this flag:
// config/bootstrap.php, after the existing
// `Configure::load('app', 'default', false);` block.
// After loading the stock config file,
// load the environment config file
// and the local config file (when present.)
try {
$env = getenv('APP_ENV');
Configure::load("app-{$env}", 'default');
} catch (\Exception $e) {
// It is not an error if this file is missing.
}
try {
Configure::load('app-local', 'default');
} catch (\Exception $e) {
// It is not an error if this file is missing.
}
This will cause our app to look for a config/app-staging.php
file, followed by a config/app-local.php
file. It’s important to point out that we want to let the app continue to bootstrap if either of these files are missing. We’ll essentially just be running with the “stock” config, which should be set up for your most important environment (probably production). This way if any environment has its flag mis-configured, your app will treat it like production. The benefit to this is that no special environment is even required in production, which again reduces the possibility for human-error.
That’s really it. We can now call Configure::read('Any.key')
and get the value that was defined in app-local, or app-$APP_ENV, or app, in that order. We’ll commit everything to source control except for config/app-local.php
which can be used in production for sensitive keys and passwords, or by developers in their local environments to test things out without modifying the “real” configs. It’s now easy to get an idea of all of the different environments the Cake app is expected to run under just by scanning the config/
directory.
A Final Example
To help illustrate how this can help clean up your code, I present a final before and after example:
Before:
// View/Helper/UtilityHelper.php
public function envHint($env = null) {
$env = (isset($_SERVER['APP_ENV']) : $_SERVER['APP_ENV'] : null);
switch ($env) {
case 'vagrant': $css = 'background: #ff9999;'; break;
case 'staging': $css = 'background: #e5c627;'; break;
default: $css = ''; break;
}
if (!empty($css) && Configure::read('debug') > 0) {
return "<style>.navbar-fixed-top{ {$css} }</style>";
}
return '';
}
This helper is “directly” env-aware– it has hard-coded environment flag values in it, and it even tries to read the flag itself (meaning that the check for the environment is duplicated here, and anywhere else in the codebase still using this old approach.)
Here’s the refactor:
// View/Helper/UtilityHelper.php
public function envHint() {
$format = (string)Configure::read('Env.Hint.Format');
$snippet = (string)Configure::read('Env.Hint.Snippet');
if (!empty($snippet) && Configure::read('debug')) {
return sprintf($format, $snippet);
}
return '';
}
Moral of the story? Move things into Configure! This approach is a lot more succinct, because the switching is done for us by the config bootstrapping already. It’s also more flexible, since we can adapt the HTML and CSS snippets used via Configure and without having to edit the method itself. This code is more in line with DRY, because the Env.Hint.Format
can be defined once in config/app.php
and all we have to do is override the Env.Hint.Snippet
value for each separate environment.
Older Cake versions
This approach actually does work pretty well in Cake 2 apps and even in Cake 1.x apps, although there are some caveats you have to address. In Cake 2 for example, your database connection settings are normally defined in Config/database.php
instead of in Configure, but it’s actually pretty easy to move those configs into Configure and make database.php
into a static loader class. An example of this is available in the Env-Awareness CakePHP 2 sample project.) This same approach works great for email.php
.
CakePHP 1.2 and 1.3 are a little trickier due to differences in how Configure::load()
behaves. Deep key merging isn’t available there, so you have to redefine top-level keys in each environment config file instead of only the differences. This can sometimes be a benefit and sometimes be a hindrance in practice.
Summary
This turns out to be a pretty good way of keeping all of the configs related to your Cake app together with the source code itself, while still being flexible enough to handle almost every situation. If for example you can’t set environment variable on your host, or edit your virtual host config, you can always use a file. If you’re afraid that a misconfigured development machine might try to send production emails, you can define critical passwords only in the app-local.php
file.
The repo contains the full slides for the original talk, as well as sample Cake 3 and Cake 2 apps that both include a pre-configured vagrant environment to play with. If you have questions, then in the spirit of open source software and open collaboration, please feel free to submit an issue.