Do not extend Zend_Config in Icinga\Application\Config

refs 
fixes 
This commit is contained in:
Johannes Meyer 2014-11-06 15:41:31 +01:00
parent f8724c504b
commit 83f386f92a
5 changed files with 658 additions and 153 deletions
library/Icinga/Application
test/php/library/Icinga/Application
ConfigTest.php
ConfigTest/files

@ -4,14 +4,18 @@
namespace Icinga\Application;
use Zend_Config;
use Zend_Config_Ini;
use Countable;
use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use LogicException;
use UnexpectedValueException;
use Icinga\Exception\NotReadableError;
/**
* Global registry of application and module configuration.
* Container for configuration values and global registry of application and module related configuration.
*/
class Config extends Zend_Config
class Config implements Countable, ArrayAccess, IteratorAggregate
{
/**
* Configuration directory where ALL (application and module) configuration is located
@ -20,13 +24,6 @@ class Config extends Zend_Config
*/
public static $configDir;
/**
* The INI file this configuration has been loaded from or should be written to
*
* @var string
*/
protected $configFile;
/**
* Application config instances per file
*
@ -42,95 +39,34 @@ class Config extends Zend_Config
protected static $modules = array();
/**
* Load configuration from the given INI file
* This config's data
*
* @param string $file The file to parse
*
* @throws NotReadableError When the file does not exist or cannot be read
* @var array
*/
public static function fromIni($file)
{
$config = new static(array(), true);
$filepath = realpath($file);
if ($filepath === false) {
$config->setConfigFile($file);
} elseif (is_readable($filepath)) {
$config->setConfigFile($filepath);
$config->merge(new Zend_Config_Ini($filepath));
} else {
throw new NotReadableError('Cannot read config file "%s". Permission denied', $filepath);
}
return $config;
}
protected $data;
/**
* Retrieve a application config instance
* The INI file this configuration has been loaded from or should be written to
*
* @param string $configname The configuration name (without ini suffix) to read and return
* @param bool $fromDisk When set true, the configuration will be read from the disk, even
* if it already has been read
*
* @return Config The configuration object that has been requested
* @var string
*/
public static function app($configname = 'config', $fromDisk = false)
{
if (!isset(self::$app[$configname]) || $fromDisk) {
self::$app[$configname] = Config::fromIni(self::resolvePath($configname . '.ini'));
}
return self::$app[$configname];
}
protected $configFile;
/**
* Set module config
* Create a new config
*
* @param string $moduleName
* @param string $configName
* @param Zend_Config $config
* @param array $data The data to initialize the new config with
*/
public static function setModuleConfig($moduleName, $configName, Zend_Config $config)
public function __construct(array $data = array())
{
self::$modules[$moduleName][$configName] = $config;
}
$this->data = array();
/**
* Retrieve a module config instance
*
* @param string $modulename The name of the module to look for configurations
* @param string $configname The configuration name (without ini suffix) to read and return
* @param string $fromDisk Whether to read the configuration from disk
*
* @return Config The configuration object that has been requested
*/
public static function module($modulename, $configname = 'config', $fromDisk = false)
{
if (!isset(self::$modules[$modulename])) {
self::$modules[$modulename] = array();
}
$moduleConfigs = self::$modules[$modulename];
if (!isset($moduleConfigs[$configname]) || $fromDisk) {
$moduleConfigs[$configname] = Config::fromIni(
self::resolvePath('modules/' . $modulename . '/' . $configname . '.ini')
);
}
return $moduleConfigs[$configname];
}
/**
* Retrieve names of accessible sections or properties
*
* @param $name
* @return array
*/
public function keys($name = null)
{
if ($name === null) {
return array_keys($this->toArray());
} elseif ($this->$name === null) {
return array();
} else {
return array_keys($this->$name->toArray());
foreach ($data as $key => $value) {
if (is_array($value)) {
$this->data[$key] = new static($value);
} else {
$this->data[$key] = $value;
}
}
}
@ -158,13 +94,335 @@ class Config extends Zend_Config
}
/**
* Prepend configuration base dir if input is relative
* Deep clone this config
*/
public function __clone()
{
$array = array();
foreach ($this->data as $key => $value) {
if ($value instanceof self) {
$array[$key] = clone $value;
} else {
$array[$key] = $value;
}
}
$this->data = $array;
}
/**
* Return the count of available sections and properties
*
* @param string $path Input path
* @return string Absolute path
* @return int
*/
public function count()
{
return count($this->data);
}
/**
* Return a iterator for this config's data
*
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->data);
}
/**
* Return whether the given section or property is set
*
* @param string $key The name of the section or property
*
* @return bool
*/
public function __isset($key)
{
return isset($this->data[$key]);
}
/**
* Return the value for the given property or the config for the given section
*
* @param string $key The name of the property or section
*
* @return mixed|NULL The value or NULL in case $key does not exist
*/
public function __get($key)
{
if (array_key_exists($key, $this->data)) {
return $this->data[$key];
}
}
/**
* Add a new property or section
*
* @param string $key The name of the new property or section
* @param mixed $value The value to set for the new property or section
*/
public function __set($key, $value)
{
if (is_array($value)) {
$this->data[$key] = new static($value);
} else {
$this->data[$key] = $value;
}
}
/**
* Remove the given property or section
*
* @param string $key The property or section to remove
*/
public function __unset($key)
{
unset($this->data[$key]);
}
/**
* Return whether the given section or property is set
*
* @param string $key The name of the section or property
*
* @return bool
*/
public function offsetExists($key)
{
return isset($this->$key);
}
/**
* Return the value for the given property or the config for the given section
*
* @param string $key The name of the property or section
*
* @return mixed|NULL The value or NULL in case $key does not exist
*/
public function offsetGet($key)
{
return $this->$key;
}
/**
* Add a new property or section
*
* @param string $key The name of the new property or section
* @param mixed $value The value to set for the new property or section
*/
public function offsetSet($key, $value)
{
if ($key === null) {
throw new LogicException('Appending values without an explicit key is not supported');
}
$this->$key = $value;
}
/**
* Remove the given property or section
*
* @param string $key The property or section to remove
*/
public function offsetUnset($key)
{
unset($this->$key);
}
/**
* Return whether this config has any data
*
* @return bool
*/
public function isEmpty()
{
return $this->count() === 0;
}
/**
* Return the value for the given property or the config for the given section
*
* @param string $key The name of the property or section
* @param mixed $default The value to return in case the property or section is missing
*
* @return mixed
*/
public function get($key, $default = null)
{
$value = $this->$key;
if ($default !== null && $value === null) {
$value = $default;
}
return $value;
}
/**
* Return all section and property names
*
* @return array
*/
public function keys()
{
return array_keys($this->data);
}
/**
* Return this config's data as associative array
*
* @return array
*/
public function toArray()
{
$array = array();
foreach ($this->data as $key => $value) {
if ($value instanceof self) {
$array[$key] = $value->toArray();
} else {
$array[$key] = $value;
}
}
return $array;
}
/**
* Merge the given data with this config
*
* @param array|Config $data An array or a config
*
* @return self
*/
public function merge($data)
{
if ($data instanceof self) {
$data = $data->toArray();
}
foreach ($data as $key => $value) {
if (array_key_exists($key, $this->data)) {
if (is_array($value)) {
if ($this->data[$key] instanceof self) {
$this->data[$key]->merge($value);
} else {
$this->data[$key] = new static($value);
}
} else {
$this->data[$key] = $value;
}
} else {
$this->data[$key] = is_array($value) ? new static($value) : $value;
}
}
}
/**
* Return the value from a section's property
*
* @param string $section The section where the given property can be found
* @param string $key The section's property to fetch the value from
* @param mixed $default The value to return in case the section or the property is missing
*
* @return mixed
*
* @throws UnexpectedValueException In case the given section does not hold any configuration
*/
public function fromSection($section, $key, $default = null)
{
$value = $this->$section;
if ($value instanceof self) {
$value = $value->$key;
} elseif ($value !== null) {
throw new UnexpectedValueException(
sprintf('Value "%s" is not of type "Config" or a sub-type of it', $value)
);
}
if ($default !== null) {
$value = $default;
}
return $value;
}
/**
* Load configuration from the given INI file
*
* @param string $file The file to parse
*
* @throws NotReadableError When the file does not exist or cannot be read
*/
public static function fromIni($file)
{
$config = new static();
$filepath = realpath($file);
if ($filepath === false) {
$config->setConfigFile($file);
} elseif (is_readable($filepath)) {
$config->setConfigFile($filepath);
$config->merge(parse_ini_file($filepath, true, INI_SCANNER_RAW));
} else {
throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath);
}
return $config;
}
/**
* Prepend configuration base dir to the given relative path
*
* @param string $path A relative path
*
* @return string
*/
public static function resolvePath($path)
{
return self::$configDir . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
}
/**
* Retrieve a application config
*
* @param string $configname The configuration name (without ini suffix) to read and return
* @param bool $fromDisk When set true, the configuration will be read from disk, even
* if it already has been read
*
* @return Config The requested configuration
*/
public static function app($configname = 'config', $fromDisk = false)
{
if (!isset(self::$app[$configname]) || $fromDisk) {
self::$app[$configname] = static::fromIni(static::resolvePath($configname . '.ini'));
}
return self::$app[$configname];
}
/**
* Retrieve a module config
*
* @param string $modulename The name of the module where to look for the requested configuration
* @param string $configname The configuration name (without ini suffix) to read and return
* @param string $fromDisk When set true, the configuration will be read from disk, even
* if it already has been read
*
* @return Config The requested configuration
*/
public static function module($modulename, $configname = 'config', $fromDisk = false)
{
if (!isset(self::$modules[$modulename])) {
self::$modules[$modulename] = array();
}
$moduleConfigs = self::$modules[$modulename];
if (!isset($moduleConfigs[$configname]) || $fromDisk) {
$moduleConfigs[$configname] = static::fromIni(
static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini')
);
}
return $moduleConfigs[$configname];
}
}

@ -5,7 +5,7 @@
namespace Tests\Icinga\Application;
use Icinga\Test\BaseTestCase;
use Icinga\Application\Config as IcingaConfig;
use Icinga\Application\Config;
class ConfigTest extends BaseTestCase
{
@ -15,8 +15,8 @@ class ConfigTest extends BaseTestCase
public function setUp()
{
parent::setUp();
$this->configDir = IcingaConfig::$configDir;
IcingaConfig::$configDir = dirname(__FILE__) . '/ConfigTest/files';
$this->oldConfigDir = Config::$configDir;
Config::$configDir = dirname(__FILE__) . '/ConfigTest/files';
}
/**
@ -25,77 +25,327 @@ class ConfigTest extends BaseTestCase
public function tearDown()
{
parent::tearDown();
IcingaConfig::$configDir = $this->configDir;
Config::$configDir = $this->oldConfigDir;
}
public function testAppConfig()
public function testWhetherInitializingAConfigWithAssociativeArraysCreatesHierarchicalConfigObjects()
{
$config = IcingaConfig::app('config', true);
$this->assertEquals(1, $config->logging->enable, 'Unexpected value retrieved from config file');
// Test non-existent property where null is the default value
$this->assertEquals(
null,
$config->logging->get('disable'),
'Unexpected default value for non-existent properties'
$config = new Config(array(
'a' => 'b',
'c' => 'd',
'e' => array(
'f' => 'g',
'h' => 'i',
'j' => array(
'k' => 'l',
'm' => 'n'
)
)
));
$this->assertInstanceOf(
get_class($config),
$config->e,
'Config::__construct() does not accept two dimensional arrays'
);
// Test non-existent property using zero as the default value
$this->assertEquals(0, $config->logging->get('disable', 0));
// Test retrieve full section
$this->assertInstanceOf(
get_class($config),
$config->e->j,
'Config::__construct() does not accept multi dimensional arrays'
);
}
/**
* @depends testWhetherInitializingAConfigWithAssociativeArraysCreatesHierarchicalConfigObjects
*/
public function testWhetherItIsPossibleToCloneConfigObjects()
{
$config = new Config(array(
'a' => 'b',
'c' => array(
'd' => 'e'
)
));
$newConfig = clone $config;
$this->assertNotSame(
$config,
$newConfig,
'Shallow cloning objects of type Config does not seem to work properly'
);
$this->assertNotSame(
$config->c,
$newConfig->c,
'Deep cloning objects of type Config does not seem to work properly'
);
}
public function testWhetherConfigObjectsAreCountable()
{
$config = new Config(array('a' => 'b', 'c' => array('d' => 'e')));
$this->assertInstanceOf('Countable', $config, 'Config objects do not implement interface `Countable\'');
$this->assertEquals(2, $config->count(), 'Config objects do not count properties and sections correctly');
}
public function testWhetherOneCanCheckWhetherConfigObjectsHaveACertainPropertyOrSection()
{
$config = new Config(array('a' => 'b', 'c' => array('d' => 'e')));
$this->assertTrue(isset($config->a), 'Config objects do not seem to implement __isset() properly');
$this->assertTrue(isset($config->c->d), 'Config objects do not seem to implement __isset() properly');
$this->assertFalse(isset($config->d), 'Config objects do not seem to implement __isset() properly');
$this->assertFalse(isset($config->c->e), 'Config objects do not seem to implement __isset() properly');
$this->assertTrue(isset($config['a']), 'Config object do not seem to implement offsetExists() properly');
$this->assertFalse(isset($config['d']), 'Config object do not seem to implement offsetExists() properly');
}
public function testWhetherItIsPossibleToAccessProperties()
{
$config = new Config(array('a' => 'b', 'c' => null));
$this->assertEquals('b', $config->a, 'Config objects do not allow property access');
$this->assertNull($config['c'], 'Config objects do not allow offset access');
$this->assertNull($config->d, 'Config objects do not return NULL as default');
}
public function testWhetherItIsPossibleToSetPropertiesAndSections()
{
$config = new Config();
$config->a = 'b';
$config['c'] = array('d' => 'e');
$this->assertTrue(isset($config->a), 'Config objects do not allow to set properties');
$this->assertTrue(isset($config->c), 'Config objects do not allow to set offsets');
$this->assertInstanceOf(
get_class($config),
$config->c,
'Config objects do not convert arrays to config objects when set'
);
}
/**
* @expectedException LogicException
*/
public function testWhetherItIsNotPossibleToAppendProperties()
{
$config = new Config();
$config[] = 'test';
}
public function testWhetherItIsPossibleToUnsetPropertiesAndSections()
{
$config = new Config(array('a' => 'b', 'c' => array('d' => 'e')));
unset($config->a);
unset($config['c']);
$this->assertFalse(isset($config->a), 'Config objects do not allow to unset properties');
$this->assertFalse(isset($config->c), 'Config objects do not allow to unset sections');
}
/**
* @depends testWhetherConfigObjectsAreCountable
*/
public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections()
{
$config = new Config();
$this->assertTrue($config->isEmpty(), 'Config objects do not report that they are empty');
$config->test = 'test';
$this->assertFalse($config->isEmpty(), 'Config objects do report that they are empty although they are not');
}
/**
* @depends testWhetherItIsPossibleToAccessProperties
*/
public function testWhetherItIsPossibleToRetrieveDefaultValuesForNonExistentPropertiesOrSections()
{
$config = new Config(array('a' => 'b'));
$this->assertEquals(
'b',
$config->get('a'),
'Config objects do not return the actual value of existing properties'
);
$this->assertNull(
$config->get('b'),
'Config objects do not return NULL as default for non-existent properties'
);
$this->assertEquals(
'test',
$config->get('test', 'test'),
'Config objects do not allow to define the default value to return for non-existent properties'
);
}
public function testWhetherItIsPossibleToRetrieveAllPropertyAndSectionNames()
{
$config = new Config(array('a' => 'b', 'c' => array('d' => 'e')));
$this->assertEquals(
array('a', 'c'),
$config->keys(),
'Config objects do not list property and section names correctly'
);
}
public function testWhetherConfigObjectsCanBeConvertedToArrays()
{
$config = new Config(array('a' => 'b', 'c' => array('d' => 'e')));
$this->assertEquals(
array('a' => 'b', 'c' => array('d' => 'e')),
$config->toArray(),
'Config objects cannot be correctly converted to arrays'
);
}
/**
* @depends testWhetherConfigObjectsCanBeConvertedToArrays
*/
public function testWhetherItIsPossibleToMergeConfigObjects()
{
$config = new Config(array('a' => 'b'));
$config->merge(array('a' => 'bb', 'c' => 'd', 'e' => array('f' => 'g')));
$this->assertEquals(
array('a' => 'bb', 'c' => 'd', 'e' => array('f' => 'g')),
$config->toArray(),
'Config objects cannot be extended with arrays'
);
$config->merge(new Config(array('c' => array('d' => 'ee'), 'e' => array('h' => 'i'))));
$this->assertEquals(
array('a' => 'bb', 'c' => array('d' => 'ee'), 'e' => array('f' => 'g', 'h' => 'i')),
$config->toArray(),
'Config objects cannot be extended with other Config objects'
);
}
/**
* @depends testWhetherItIsPossibleToAccessProperties
*/
public function testWhetherItIsPossibleToDirectlyRetrieveASectionProperty()
{
$config = new Config(array('a' => array('b' => 'c')));
$this->assertEquals(
'c',
$config->fromSection('a', 'b'),
'Config::fromSection does not return the actual value of a section\'s property'
);
$this->assertNull(
$config->fromSection('a', 'c'),
'Config::fromSection does not return NULL as default for non-existent section properties'
);
$this->assertNull(
$config->fromSection('b', 'c'),
'Config::fromSection does not return NULL as default for non-existent sections'
);
$this->assertEquals(
'test',
$config->fromSection('a', 'c', 'test'),
'Config::fromSection does not return the given default value for non-existent section properties'
);
}
/**
* @expectedException UnexpectedValueException
* @depends testWhetherItIsPossibleToAccessProperties
*/
public function testWhetherAnExceptionIsThrownWhenTryingToAccessASectionPropertyOnANonSection()
{
$config = new Config(array('a' => 'b'));
$config->fromSection('a', 'b');
}
public function testWhetherConfigResolvePathReturnsValidAbsolutePaths()
{
$this->assertEquals(
Config::$configDir . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b.ini',
Config::resolvePath(DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b.ini'),
'Config::resolvePath does not produce valid absolute paths'
);
}
/**
* @depends testWhetherConfigObjectsCanBeConvertedToArrays
* @depends testWhetherConfigResolvePathReturnsValidAbsolutePaths
*/
public function testWhetherItIsPossibleToInitializeAConfigObjectFromAIniFile()
{
$config = Config::fromIni(Config::resolvePath('config.ini'));
$this->assertEquals(
array(
'disable' => 1,
'db' => array(
'user' => 'user',
'password' => 'password'
'logging' => array(
'enable' => 1
),
'backend' => array(
'type' => 'db',
'user' => 'user',
'password' => 'password',
'disable' => 1
)
),
$config->backend->toArray()
$config->toArray(),
'Config::fromIni does not load INI files correctly'
);
// Test non-existent section using 'default' as default value
$this->assertEquals('default', $config->get('magic', 'default'));
// Test sub-properties
$this->assertEquals('user', $config->backend->db->user);
// Test non-existent sub-property using 'UTF-8' as the default value
$this->assertEquals('UTF-8', $config->backend->db->get('encoding', 'UTF-8'));
// Test invalid property names using false as default value
$this->assertEquals(false, $config->backend->get('.', false));
$this->assertEquals(false, $config->backend->get('db.', false));
$this->assertEquals(false, $config->backend->get('.user', false));
// Test retrieve array of sub-properties
$this->assertInstanceOf(
get_class($config),
Config::fromIni('nichda'),
'Config::fromIni does not return empty configs for non-existent configuration files'
);
}
/**
* @expectedException Icinga\Exception\NotReadableError
*/
public function testWhetherFromIniThrowsAnExceptionOnInsufficientPermission()
{
Config::fromIni('/etc/shadow');
}
/**
* @depends testWhetherItIsPossibleToInitializeAConfigObjectFromAIniFile
*/
public function testWhetherItIsPossibleToRetrieveApplicationConfiguration()
{
$config = Config::app();
$this->assertEquals(
array(
'user' => 'user',
'password' => 'password'
'logging' => array(
'enable' => 1
),
'backend' => array(
'type' => 'db',
'user' => 'user',
'password' => 'password',
'disable' => 1
)
),
$config->backend->db->toArray()
$config->toArray(),
'Config::app does not load INI files correctly'
);
// Test singleton
$this->assertEquals($config, IcingaConfig::app('config'));
$this->assertEquals(array('logging', 'backend'), $config->keys());
$this->assertEquals(array('enable'), $config->keys('logging'));
}
public function testAppExtraConfig()
/**
* @depends testWhetherItIsPossibleToInitializeAConfigObjectFromAIniFile
*/
public function testWhetherItIsPossibleToRetrieveModuleConfiguration()
{
$extraConfig = IcingaConfig::app('extra', true);
$this->assertEquals(1, $extraConfig->meta->version);
$this->assertEquals($extraConfig, IcingaConfig::app('extra'));
}
$config = Config::module('amodule');
public function testModuleConfig()
{
$moduleConfig = IcingaConfig::module('amodule', 'config', true);
$this->assertEquals(1, $moduleConfig->menu->get('breadcrumb'));
$this->assertEquals($moduleConfig, IcingaConfig::module('amodule'));
}
public function testModuleExtraConfig()
{
$moduleExtraConfig = IcingaConfig::module('amodule', 'extra', true);
$this->assertEquals(
'inetOrgPerson',
$moduleExtraConfig->ldap->user->get('ldap_object_class')
array(
'menu' => array(
'breadcrumb' => 1
)
),
$config->toArray(),
'Config::module does not load INI files correctly'
);
$this->assertEquals($moduleExtraConfig, IcingaConfig::module('amodule', 'extra'));
}
}

@ -2,6 +2,7 @@
enable = 1
[backend]
db.user = 'user'
db.password = 'password'
type = "db"
user = "user"
password = "password"
disable = 1

@ -1,2 +0,0 @@
[ldap]
user.ldap_object_class = inetOrgPerson