Environment bootstrapping with Titon\Environment

Handling multiple environments in an application can be tedious as each environment supplies its own set of configuration and unique bootstrapping. The amount of environments can grow to unmanageable numbers when dealing with dev environments, for example, different configuration for each developer. With no external dependencies, and a basic requirement of PHP 5.3, the Titon\Environment package hopes to solve this problem.

Defining Environments

An environment is mapped using an HTTP host or an IP address. During an HTTP request, an environment will be detected that matches the current host/IP, which is then loaded and bootstrapped. But first the environments need to be added through addHost(), which accepts a unique key, an array of hosts and IPs to match against, and finally the type. Out of the box, the package supports 3 types of environments — dev, staging and prod.

use Titon\Environment\Environment;
use Titon\Environment\Host;
// Create an environment instance
$env = new Environment();
// Register a dev env
$env->addHost(new Host('dev', array('127.0.0.1', '::1', 'localhost'), Environment::DEVELOPMENT));

What this did is create a development host environment for the localhost host, the 127.0.0.1 v4 IP, and the ::1 v6 IP. When either of those values are matched against the current request, the environment will be bootstrapped. But where does the bootstrapping take place? We must define it first! This can be accomplished through the setBootstrap() method on the Titon\Environment\Host object. Using the same example above, an absolute path to a bootstrap file can be defined.

$env->addHost(new Host('dev', array('127.0.0.1', '::1', 'localhost'), Environment::DEVELOPMENT))
	->setBootstrap('/absolute/path/to/bootstrap.php');

Now when an environment is detected a bootstrap will occur. This bootstrap file should contain configuration and logic specific to each environment.

Multiple Environments

In the previous example only one environment was added, dev. In standard applications, at minimum 2 environments will exist, dev and prod. Let's add the prod environment through the production domain and include a fallback. A fallback is used when an environment cannot be matched — this usually will fallback to prod as there is no risk of dev code making it into production, or at minimum a dev environment with error reporting turned off.

$env->addHost(new Host('prod', 'website.com', Environment::PRODUCTION))->setBootstrap('/envs/prod.php');
// Set a fallback using the host key
$env->setFallback('prod');

Let's now solve the multiple developer issue mentioned in the opening paragraph.

// Share the default boostrap
$env->addHost(new Host('john', 'john.dev.website.com', Environment::DEVELOPMENT))->setBootstrap('/envs/dev.php');
$env->addHost(new Host('mike', 'mike.dev.website.com', Environment::DEVELOPMENT))->setBootstrap('/envs/dev.php');
// Custom boostrap
$env->addHost(new Host('chris', 'chris.dev.website.com', Environment::DEVELOPMENT))->setBootstrap('/envs/dev-chris.php');

Or perhaps multiple QA staging environments for different regions?

foreach (array('us', 'eu', 'cn', 'au') as $region) {
	$env->addHost(new Host('qa-' . $region, $region. '.qa.website.com', Environment::STAGING))->setBootstrap('/envs/qa-' . $region. '.php');
}
Initializing An Environment

Once all the environment hosts have been defined, the package must be initialized. This can be done through the initialize() method, which will kick-start the matching and bootstrapping process.

$env->initialize();

Once an environment has been chosen, you can access it at anytime.

// Return the current host
$env->current();
// Or values from the host
$env->current()->getKey();
$env->current()->isStaging();
// Or use the convenience methods
$env->isDevelopment();
In Closing

For being such a lightweight class, the Titon\Environment package provides an extremely helpful application pattern. Be sure to give it a try in any MVC framework!

Plugin loading quirks

On a daily basis I will receive emails pertaining to my Admin plugin, Utility plugin or my Forum plugin. Most of the time the problem they are having is related to plugin loading, and the failure to bootstrap or apply routes. But loading plugins is easy! One would think so, it does look pretty straight forward, but there are some weird cases where loading just does not work as you would assume.

In my documentation, I state that plugin loading should be done after CakePlugin::loadAll(), for the reasons listed below.

Using loadAll()

By default, CakePlugin::loadAll() does not include bootstrap or routes files. Passing an array with bootstrap and routes set to true will include them, but any plugin that does not have those files will throw missing errors. A setting ignoreMissing was added in CakePHP 2.3.0 to solve this. This approach works correctly, but not for versions below 2.3.0.

CakePlugin::loadAll(array(
	array('bootstrap' => true, 'routes' => true, 'ignoreMissing' => true)
));
Calling loadAll() last

One would think that calling CakePlugin::loadAll() after other plugins would work normally, but that's wrong. When called this way, previously loaded routes will be reset. This happens because loadAll() cycles through each plugin and re-loads them (even if a plugin was already loaded), and since both bootstrap and routes are false by default (point above) in loadAll(), the values get reset. Honestly, I believe this to be a bug in CakePHP, so when I have time I will track it down. Currently, this approach does not work correctly.

CakePlugin::load('Utility', array('bootstrap' => true, 'routes' => true));
CakePlugin::load('Admin', array('bootstrap' => true, 'routes' => true));
CakePlugin::load('Forum', array('bootstrap' => true, 'routes' => true));
CakePlugin::loadAll();
Calling loadAll() first

If calling it last doesn't work correctly, then calling it first will. By calling CakePlugin::loadAll() first, all the problems listed in the point above are invalid and plugin loading works correctly. Both routes and bootstrapping successfully triggers. This approach works correctly.

CakePlugin::loadAll();
CakePlugin::load('Utility', array('bootstrap' => true, 'routes' => true));
CakePlugin::load('Admin', array('bootstrap' => true, 'routes' => true));
CakePlugin::load('Forum', array('bootstrap' => true, 'routes' => true));
Passing an array to load()

Many don't know this, but an array of plugins can be passed to CakePlugin::load(). I tried many different combinations with no failures. This approach works correctly.

CakePlugin::load(array(
	'Uploader',
	'Utility' => array('bootstrap' => true, 'routes' => true),
	'Admin' => array('bootstrap' => true, 'routes' => true),
	'Forum' => array('bootstrap' => true, 'routes' => true)
));
CakePlugin::load(
	array('Utility', 'Admin', 'Forum'),
	array('bootstrap' => true, 'routes' => true)
);
How did you debug this?

It was rather easy. In Config/bootstrap.php I would attempt different variations of plugin loading to test the outcome. In my controller I had a few debug() statements that spit out Configure and Router information to verify that loading was working.

debug(App::objects('plugin'));
debug(Configure::read());
debug(Router::$routes);

Making sure debug is off in production

Over a year ago I wrote about turning debug off automatically in production. That post I wrote is completely wrong (to an extent). The theory is correct but the execution was incorrect. Even one of the comments pointed out the problem, but I haven't had time to blog about it till now.

About a month ago I realized my implementation was wrong when one of my live sites was outputting MySQL errors and database information (including passwords) to all my users. Since debug in core.php was set to 2, and then disabled to 0 in bootstrap.php, the errors were being triggered before bootstrap was loaded. This was a huge problem as it printed out vital DB information.

It is an easy fix however, simply switch around the values from my previous entry. Debug in core.php should be set to 0 and in bootstrap.php it should be set to 2! That fixes the startup errors that appear before the bootstrap process.

if (env('REMOTE_ADDR') == '127.0.0.1') {
	Configure::write('debug', 2);
}