Permalink
Fetching contributors…
Cannot retrieve contributors at this time
292 lines (248 sloc) 8.53 KB
<?php
namespace Bart\Configuration;
use Bart\Diesel;
use Bart\Log4PHP;
use Bart\Primitives\Arrays;
/**
* Configuration base. All configuration classes must extend this.
* All children are required to define a README method which can be used
* to see how each class expects its conf files to look.
*/
abstract class Configuration
{
/** @var string The base directory containing all static configurations */
private static $path = null;
private static $configCache = array();
/** @var array */
protected $configurations;
/** @var \Logger */
protected $logger;
/** @var string File path on disk whence configuration file was loaded */
private $filePath;
/**
* @return string Sample of how configuration is intended to be defined
*/
public abstract function README();
/**
* @param string $path Root path to all configuration
* @throws ConfigurationException If already configured
*/
public static function configure($path)
{
if (self::$path) {
// If this becomes innapppropriate, it can be lifted
// It feels apppropriate based on my current understanding of use cases
throw new ConfigurationException('Cannot reconfigure configuration path. Already set to ' . self::$path);
}
self::$path = $path;
}
/**
* Instantiate instance configured to load configurations based on called class name
*/
public function __construct()
{
$this->logger = Log4PHP::getLogger(get_called_class());
$this->load();
}
/**
* @param string $section Configuration section in file
* @param string $key Configuration key in section
* @param mixed $default If not required, the default to use
* @param bool $required If an exception should be raised when value is missing
* @return mixed Configured value or default
* @throws ConfigurationException
*/
protected function getValue($section, $key, $default = null, $required = true)
{
if (array_key_exists($section, $this->configurations)) {
$sectionValues = $this->configurations[$section];
if (array_key_exists($key, $sectionValues)) {
return $sectionValues[$key];
}
}
// Complain when the value is required and no default passed
// ...Provides path for non-required when the default is literally null
if ($default === null && $required) {
throw new ConfigurationException("No value set for required ${section}.${key}");
}
return $default;
}
/**
* http://stackoverflow.com/questions/12650802/php-equivalent-of-javascripts-parseint-function
*/
protected function getNumeric($section, $key, $default = null, $required = true)
{
$rawVal = $this->getValue($section, $key, $default, $required);
if (ctype_digit($rawVal)) {
return intval($rawVal);
}
if (is_numeric($rawVal)) {
return $rawVal;
}
throw new ConfigurationTypeConversionException("Non-numeric provided for ${section}.${key}");
}
/**
* @return string[] String list of value split by comma
* @throws ConfigurationTypeConversionException
*/
protected function getArray($section, $key, array $default = [], $required = true)
{
$rawVal = $this->getValue($section, $key, $default, $required);
// If there is no configured value, return the default which is already an array
if ($rawVal === $default) {
return $default;
}
// I considered accepting a "split" parameter, but decided that for now
// ...enforcing a convention of at most one space after the comma will
// ...encourage cleaner configuration files
return preg_split('/,(\s)?/', $rawVal);
}
/**
* @return bool If the value equals the literal string "true"
*/
protected function getBool($section, $key, $default = null, $required = true)
{
$value = $this->getValue($section, $key, $default, $required);
// will equal 'true' when conf is quoted, will equal '1' when literal boolean used!
// See the unit tests for more fun realities of parse_ini_*()
return ($value === 'true' || $value === '1' || $value === true);
}
/**
* @return string User name of effective user
*/
protected function getCurrentUsername()
{
// Assuming its safe to statically cache since only one user should be running the program
if (!Arrays::vod(self::$configCache, '__USERNAME__')) {
/** @var \Bart\Shell $shell */
$shell = Diesel::create('\Bart\Shell');
self::$configCache['__USERNAME__'] = $shell->get_effective_user_name();
}
return self::$configCache['__USERNAME__'];
}
/**
* Prompt user for their user account's password
* @seealso self::getSecret() for context specific secrets
* @param string $prompt Text to prompt the user
* @return string Current user's password
*/
protected function getCurrentPassword($prompt)
{
// Assuming its safe to statically cache since only one user should be running the program
// ...and user should have only one local account & password
if (!Arrays::vod(self::$configCache, '__PASSWD__')) {
/** @var \Bart\Shell $shell */
$shell = Diesel::create('\Bart\Shell');
self::$configCache['__PASSWD__'] = $shell->std_in_secret($prompt);
}
return self::$configCache['__PASSWD__'];
}
/**
* Prompt the user for secret input. Secret is cached in $section.$key for later retrieval.
* @seealso self::getCurrentPassword() for globally shared password
* @param string $section Section in which to use key to save secret in cache only
* @param string $key Key name to associate with secret in cache only
* @param string $prompt Text to prompt user input
* @return string Secret input from user
*/
protected function getSecret($section, $key, $prompt)
{
$cached = $this->getValue($section, $key, null, false);
// If we already prompted for the value
if ($cached) {
return $cached;
}
/** @var \Bart\Shell $shell */
$shell = Diesel::create('\Bart\Shell');
$secret = $shell->std_in_secret($prompt);
$this->updateRuntimeConfiguration($section, $key, $secret);
return $secret;
}
/**
* Load the configurations from the config file for subclass
* @return array The parsed array from the configuration file
*/
private function load()
{
$basePath = $this->configsPath();
if (!$basePath) {
throw new ConfigurationException('Configuration root path not set! Please call configure()');
}
$subclass = get_called_class();
$indSlash = strrpos($subclass, '\\');
if ($indSlash !== false) {
// Strip off namespace
$subclass = substr($subclass, $indSlash + 1);
}
// Strip off "Config"
$subclass = substr($subclass, 0, -1 * strlen('Config'));
// Chop any trailing underscore for non-camel cased names
$name = strtolower(chop($subclass, '_'));
$filePath = $basePath . "/$name.conf";
if (!array_key_exists($filePath, self::$configCache)) {
self::$configCache[$filePath] = $this->loadParsedIni($filePath, $name);
}
$this->filePath = $filePath;
$this->configurations = self::$configCache[$filePath];
}
/**
* Set in memory configs for $section[$key] = $value; this is used exclusively
* for caching secrets
* @param string $section
* @param string $key
* @param string $value
*/
private function updateRuntimeConfiguration($section, $key, $value)
{
$cache = self::$configCache[$this->filePath];
if (!Arrays::vod($cache, $section)) {
$cache[$section] = [];
}
// Update with the new value
$cache[$section][$key] = $value;
self::$configCache[$this->filePath] = $cache;
$this->configurations = $cache;
}
/**
* @abstract
* @return string Path to configurations
*/
protected function configsPath()
{
return self::$path;
}
/**
* @abstract
* @param string $filePath Absolute path to file containing configurations
* @param string $subclass Name of the configuration class
* @return array Contents of configuration parsed as INI with sections
* @throws ConfigurationException
*/
protected function loadParsedIni($filePath, $subclass)
{
/** @var \Bart\Shell $shell */
$shell = Diesel::create('\Bart\Shell');
if (!$shell->file_exists($filePath)) {
throw new ConfigurationException("No configuration file found for $subclass at $filePath");
}
// @NOTE we're not using the ConfigResolver to resolve environment
// ...distinctions by default. To add this ability, a new method should
// ...be added to this base to resolve and then reset @configurations
return $shell->parse_ini_file($filePath, true);
}
}
/**
* Class ConfigurationException Generic exception in Configuration package
* @package Bart\Configuration
*/
class ConfigurationException extends \Exception
{
}
/**
* Class ConfigurationTypeConversionException Exception when loading config value
* and attempting coercion to expected type
* @package Bart\Configuration
*/
class ConfigurationTypeConversionException extends \Exception
{
}