Allowing db overrides of hard-coded conf values in CakePHP

I've had this sitting in my project code for a while. After doing a bit of rework today, I thought the concept might be useful to CakePHP noobs (of which I am one).

What I wanted to do with the project was to hard-code some base config values that can be used in the app. That's easy - just drop them into a custom config file and fetch them with Configure::load and Configure::read. [Easier still, dump them into core.php or use Configure::write elsewhere (app_controller/beforeFilter?), but I wanted a central file to point potential users towards.]

It works fine this way, but if users want to modify settings, they would need to go digging into code. With a custom conf file, it would certainly make it harder for them to break the app, but there are some people who really don't need to be tweaking code no matter how "safe" it is.

With those people in mind, I've opted to allow overrides of those hard-coded conf values by setting name->value pairs in a database table and using the following function to fetch the value. 'SystemVariable' is the model that I use to hold the overrides.

/**
 * Fetches the config value for $name from database table system_variables (if $name field exists) or system conf file my_custom_conf.php.
 *
 * @param string $name Config field name to find in my_custom_conf.php or database.
 * @return mixed Null on error or not found.  Value if found.
 */
function getConfigValue($name)
{
	$rv = null;
	if (!empty($name))
	{
		$rv = ClassRegistry::init('SystemVariable')->findByName($name);
		if ($rv && isset($rv['SystemVariable']) && isset($rv['SystemVariable']['name']) && $rv['SystemVariable']['name'] == $name)
		{
			$rv = $rv['SystemVariable']['value'];
		}
		else
		{
			if (Configure::load('my_custom_conf') === false)
			{
				$rv = null;
			}
			else
			{
				$rv = Configure::read('my_custom_namespace.' . $name); // null on error
			}
		}
	}
	return $rv;
}

And I stored that in app/config/bootstrap.php to allow for global access. Why? Because I'm also using it to fetch config values for some custom model validation rules and am even using it a bit in some views (to fetch the app's name, for example). There's really not much to the function itself, obviously. But it provides the override functionality I was looking for.

There is one place that it hasn't done what I've wanted. I've set up the ability to put the system offline or into "maintenance mode". Rather than giving preference to the db value here, I preferred to OR the custom conf value with the db value so that if something is screwy in one location, I can still disable the app in the other location. That precluded the use of this function and required custom code, but since it's a one-off, it's an acceptable technique.

Here's the rest of the usage (including maintenance mode ideas from here and here):

(FYI: 'Artemis' is the custom conf file and the namespace there.)
app_controller:

	function beforeFilter()
	{
		// ...
		
		// Test whether site is online/offline.
		$this->_checkMaintenance();
		// ...
	}

	function _checkMaintenance()
	{
		// If the site is under maintenance...
		// - getConfigValue() returns the db value first, if exists.  If not, then the config value from artemis.php.
		// - But we want to OR them to allow for a "backup" method to go offline in case of a db issue.
		$app_maintenance_db   = ClassRegistry::init('SystemVariable')->findByName('app_maintenance');
		$app_maintenance_conf = Configure::load('artemis') ? Configure::read('Artemis.app_maintenance') : false;
		if ($app_maintenance_db || $app_maintenance_conf)
		{
			if ($this->action !== 'login' && $this->action !== 'logout')
			{
				$uid = $this->Auth->user('id');
				$gid = $this->Auth->user('group_id');
				// If user is authed, check to see if it's an admin.
				if ($this->Auth && $this->Acl && $uid && $gid)
				{
					// Get the group name of the logged-in user.
					$group_name = ClassRegistry::init('Group')->findById($gid);
					// If not an administrator...
					if ($group_name['Group']['name'] !== getConfigValue('admin_group'))
					{
						header('HTTP/1.1 503 Service Temporarily Unavailable');
						header('Retry-After: ' . HOUR);
						$this->cakeError('maintenance');
					}
					else // User is an administrator, so allow full access with a maintenance page heading.
					{
						$this->Session->setFlash('Maintenance mode'); // TODO RTMS: Change this to a meaningful message with a link to change status.
					}
				}
				else // User is not authed, so redirect to maintenance page.
				{
					header('HTTP/1.1 503 Service Temporarily Unavailable');
					header('Retry-After: ' . HOUR);
					$this->cakeError('maintenance');
				}
			}
		}
	}

The only other thing I needed to do that wasn't included in the cakeError-using link above was to set up AppError:

app_error.php:

<?php

class AppError extends ErrorHandler
{
	/**
	 *  Handles maintenance mode.
	 */
	function maintenance()
	{
		$this->_outputMessage('maintenance');
	}
}

?>

freetags:

Add new comment