I reuse a Config class (simplified code at the bottom) in a lot of my projects. It’s generally instantiated once in my app bootstrap file and stored in a container class from which I inject it as a dependency into objects via static “makeThis()” method calls.
Because it’s instantiated every time my apps are executed I want to streamline it as much as possible. Repeat: I’m looking to make this process as efficient as possible.
I have default config values specified in the Config::$defaults array at instantiation. What I want to optimize specifically is the assignment of properties inside the load_conf_environment() function. You’ll find code comments inside this function further elucidating my requirements.
The code below should be clear enough (to anyone capable of providing a valid answer) to illustrate how the class works. I’m looking for an imaginative and efficient way to handle this operation and I’m not averse to a massive refactoring if you can support your claim.
Finally, let me pre-emptively rule out one suggestion: NO, I’m not interested in using constants for the directory path values. I used to do that but it makes unit testing difficult.
UPDATE:
- I’ve considered using
gettype()to determine the default
value’s type and subsequently cast the passed value, but according to
the php docs this is not a good solution. - Currently I’m storing the different config directive names in arrays
by type and doing a check to see which array contains the directive
name and perform a typecast or assign values based on that. This seems “ugly” to me
and is what I’m trying to get away from if possible.
UPDATE2:
Having config values match the variable type of the defaults is the ideal situation, but I kind of just got it into my head that I wanted it that way … it may not even really matter. Maybe it should just be incumbent upon the coder to provide valid configuration values? Anyone have any thoughts on that?
<?php
class Config
{
/**
* The base application directory in which all app_dirs reside
*/
protected $app_path;
/**
* Configuration directives
*
* @var array
* @access protected
*/
protected $vals = array();
/**
* List of default config directive values
*
* @var array
* @access protected
*/
protected $defaults;
/**
* Class constructor -- basically sets default values
*
* @param string $app_path Base application directory
*
* @return void
*/
public function __construct($app_path=NULL)
{
$this->defaults = array(
'debug' => FALSE,
'autoload' => FALSE,
'is_cli_app' => FALSE,
'is_web_app' => FALSE,
'smarty' => FALSE,
'phpar' => FALSE,
'front_url' => '',
'app_dir_bin' => 'bin',
'app_dir_conf' => 'conf',
'app_dir_docs' => 'docs',
'app_dir_controllers' => 'controllers',
'app_dir_lib' => 'lib',
'app_dir_models' => 'models',
'app_dir_test' => 'test',
'app_dir_vendors' => 'vendors',
'app_dir_views' => 'views',
'app_dir_webroot' => 'webroot',
);
if ($app_dir) {
$this->set_app_path($app_path);
}
}
/**
* Setter function for $app_path property
*
* @param string $app_path Path to the app on the server
*
* @return void
* @throws ConfigException On unreadable/nonexistent path
*/
public function set_app_path($app_path)
{
$app_path = dirname(realpath((string)$app_path));
if (is_readable($app_path)) {
$this->app_path = $app_path;
} else {
$msg = 'Specified app path directory could not be read';
throw new ConfigException($msg);
}
}
/**
* Set default values for uninitialized directives
*
* Called after $cfg file is read and applied to fill
* out any unspecified directives.
*
* @return void
*/
protected function set_defaults()
{
foreach ($this->defaults as $key => $val) {
if ( ! isset($this->vals[$key])) {
$this->vals[$key] = $this->is_app_dir($key)
? $this->app_dir . '/' . $this->defaults[$key]
: $this->defaults[$key];
}
}
}
/**
* (Re-)Loads configuration directives
*
* @param mixed $cfg Config array or directory path to config.php
*
* @throws ConfigException On invalid config array
* @return void
*/
public function load_conf_environment($cfg)
{
// Reset all configuration directives
$this->vals = array();
if ( ! is_array($cfg)) {
$cfg = $this->get_cfg_arr_from_file($cfg);
}
if (empty($cfg)) {
$msg = 'A valid $cfg array or environment path is required for ' .
'environment configuration';
throw new ConfigException($msg);
}
foreach ($cfg as $name => $val) {
// does a setter method exist for this directive?
$method = "set_$name";
if (method_exists($this, $method)) {
$this->$method($val);
continue;
}
/*
ASSIGN SPECIFIED DIRECTIVE VALUE
THE ASSIGNED VALUE SHOULD BE OF THE SAME TYPE SPECIFIED by $this->defaults
IF DIRECTIVE IS ONE OF THE 'app_dir' VARS:
- IF IT'S A VALID ABSOLUTE PATH, SET THAT AS THE VALUE
- OTHERWISE, app_dir PATHS SHOULD BE RELATIVE TO $this->app_path
I CONSIDERED USING gettype() TO DETERMINE THE DEFAULT VAR TYPE
AND THEN CAST THE VALUE, BUT THE DOCS SAY THIS IS A BAD IDEA.
*/
}
// set default vals for any directives that weren't specified
$this->set_defaults();
if ( ! $this->vals['is_cli_app'] && ! $this->vals['is_web_app']) {
$msg = 'App must be specified as either WEB or CLI';
throw new Rasmus\ConfigException($msg);
}
}
/**
* Load a configuration array from a specified file
*
* If no valid configuration file can be found,
* an empty array is returned. Valid config paths
*
* @param string $path Config environment directory path
*
* @return array List of config key=>value pairs
* @throws ConfigException On invalid environment path
*/
protected function get_cfg_arr_from_file($path)
{
$path = (string)$path;
$path = rtrim($path, DIRECTORY_SEPARATOR) . '/config.php';
if (is_readable($path)) {
require $path;
if (is_array($cfg) && ! empty($cfg)) {
return $cfg;
}
}
return array();
}
/**
* Has the specified config directive been loaded?
*
* @param string $directive Configuration directive name
*
* @return bool On whether or not a specific directive exists
*/
public function is_loaded($directive)
{
return isset($this->vals[$directive]);
}
/**
* Retrieves a key=>value list of config directives
*
* @return array List of configuration directives
*/
public function get_directives()
{
return $this->vals;
}
/**
* Magic method mapping object properties to $vals array
*
* @param string $name Object property name
*
* @return mixed
*/
public function __get($name)
{
if (isset($this->vals[$name])) {
return $this->vals[$name];
} else {
$msg = "Invalid property: $name is not a valid configuration directive";
throw new OutOfBoundsException($msg);
}
}
}
?>
Handling default values
Is there a reason why you set defaults for directives that need them after loading values from the config file?
Since the defaults are already hard-coded in the
__construct, why not just move them to the$valsproperty and avoid needing to set them later on? This way, yourload_conf_environmentmethod overwrites the default value that already exists for the directive.Validating types
This is something I would enforce with each directive’s setter method. Each setter should verify the type of value with the
is_*functions or by type-hinting in the setter arguments and throw exceptions accordingly. I would probably go so far as to require each directive have a setter. It’s more work, but cleaner and 100% enforceable.Handling app_dir directives (one option)
set_app_pathis called, run through the vals property and call theset_app_dir_directivemethod (see below) on the app_dir directives so that they are prefixed with the app path.Make a new
set_app_dir_directivemethod: