--- /dev/null
+nbproject
+vendor
--- /dev/null
+language: php
+
+php:
+ - 5.5
+ - 5.6
+ - hhvm
+
+before_script: composer install
+
+script: phpunit --coverage-text --color tests
--- /dev/null
+Copyright (c) 2014, Michael Wallner <mike@php.net>.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
--- /dev/null
+= merry\\Config
+
+A merry configuration container.
+
+Example:
+```php
+use merry\Config;
+
+$config = new Config([
+ "db" => [
+ "dsn" => "user=mike",
+ "flags" => pq\Connection::PERSISTENT
+ ],
+ "cache" => [
+ "pid" => "cluster1",
+ "hosts" => ["10.0.1.1", "10.0.1.2", "10.0.1.3"]
+ ]
+]);
+
+printf("Using database: '%s'\n", $config->db->dsn);
+printf("Using cache cluster: '%s'\n", $config->cache->pid);
+
+$config->apply([
+ "db" => function($conf) {
+ return new pq\Connection($conf->dsn, $conf->flags);
+ },
+ "cache" => function($conf) {
+ $cache = new Memcached($conf->pid);
+ foreach ($conf->{$conf->pid}->hosts as $host) {
+ $cache->addServer($host);
+ }
+ return $cache;
+ }
+]);
+
+
+extract($config->toArray());
+
+if (!($q1 = $cache->get("q1"))) {
+ $result = $db->exec("SELECT 1");
+ $cache->set("q1", $q1 = $result->fetchAll());
+}
+```
+
+Another example:
+```php
+use merry\Config;
+
+$array = parse_ini_string('
+[localhost]
+db.dsn = "user=mike"
+db.flags = 2 ;pq\Connection::PERSISTENT
+cache.pid = "cluster1"
+cache.cluster1.hosts[] = "10.0.1.1"
+cache.cluster1.hosts[] = "10.0.1.2"
+cache.cluster1.hosts[] = "10.0.1.3"
+[production : localhost]
+db.dsn = "user=app"
+');
+
+$config = new Config($array, getenv("APPLICATION_ENV"));
+$flags = \RecursiveTreeIterator::BYPASS_CURRENT;
+foreach (new \RecursiveTreeIterator($config, $flags) as $key => $val ) {
+ printf("%s: %s\n", $key, ($val instanceof Config) ? "" : $val);
+}
+```
+
+Output:
+```
+|-db:
+| |-dsn: user=app
+| \-flags: 2
+\-cache:
+ |-pid: cluster1
+ \-cluster1:
+ \-hosts:
+ |-0: 10.0.1.1
+ |-1: 10.0.1.2
+ \-2: 10.0.1.3
+```
\ No newline at end of file
--- /dev/null
+{
+ "name": "m6w6/merry",
+ "type": "library",
+ "description": "Merry Configuration Container",
+ "keywords": ["merry", "config", "configuration", "container"],
+ "homepage": "http://github.com/m6w6/merry",
+ "license": "BSD-2-Clause",
+ "authors": [
+ {
+ "name": "Michael Wallner",
+ "email": "mike@php.net"
+ }
+ ],
+ "autoload": {
+ "psr-0": {
+ "merry\\Config": "lib"
+ }
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * merry\Config
+ *
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace merry;
+
+/**
+ * A merry config container
+ *
+ * @see https://github.com/m6w6/merry
+ * @package merry\Config
+ */
+class Config implements \ArrayAccess, \RecursiveIterator
+{
+ /**
+ * Index for a numerically indexed array
+ * @internal
+ * @var int
+ */
+ private $index = 0;
+
+ /**
+ * Container
+ * @internal
+ * @var stdClass
+ */
+ private $props;
+
+ /**
+ * State for the RecursiveIterator
+ * @internal
+ * @var array
+ */
+ private $riter;
+
+ /**
+ * Create a new configuration container
+ * @param array $array the configuration array
+ * @param string $section the section to use (i.e. first level key)
+ * @param string $section_sep a separator for section extension
+ * @param string $key_sep a separator for key traversal
+ */
+ public function __construct(array $array = null, $section = null, $section_sep = ":", $key_sep = ".") {
+ $this->props = new \stdClass;
+
+ if (isset($section) && strlen($section_sep)) {
+ $array = $this->combine($array, $section_sep)[$section];
+ }
+ if ($array) {
+ $config = array();
+
+ if (strlen($key_sep)) {
+ foreach ($array as $key => $val) {
+ $this->walk($config, $key, $val, $key_sep);
+ }
+ }
+
+ foreach ($config as $property => $value) {
+ $this->__set($property, $value);
+ }
+ }
+ }
+
+ /**
+ * Combine individual sections with their parent section
+ * @param array $array the config array
+ * @param string $section_sep the section extension separator
+ * @return array merged sections
+ */
+ protected function combine($array, $section_sep) {
+ foreach ($array as $section_spec => $settings) {
+ $section_spec = array_map("trim", explode($section_sep, $section_spec));
+ if (count($section_spec) > 1) {
+ $sections[$section_spec[0]] = array_merge(
+ $sections[$section_spec[1]],
+ $settings
+ );
+ } else {
+ $sections[$section_spec[0]] = $settings;
+ }
+ }
+ return $sections;
+ }
+
+ /**
+ * Walk a key split by the key separator into an array up and set the
+ * respective value on the leaf
+ * @param mixed $ptr current leaf pointer in the array
+ * @param string $key the array key
+ * @param mixed $val the value to set
+ * @param string $key_sep the key separator for traversal
+ */
+ protected function walk(&$ptr, $key, $val, $key_sep) {
+ foreach (explode($key_sep, $key) as $sub) {
+ $ptr = &$ptr[$sub];
+ }
+ $ptr = $val;
+ }
+
+ /**
+ * Recursively turn a Config instance and its childs into an array
+ * @param \merry\Config $o the Config instance to convert to an array
+ * @return array
+ */
+ protected function arrayify(Config $o) {
+ $a = [];
+
+ foreach ($o->props as $k => $v) {
+ if ($v instanceof Config) {
+ $a[$k] = $this->arrayify($v);
+ } else {
+ $a[$k] = $v;
+ }
+ }
+
+ return $a;
+ }
+
+ /**
+ * Apply one or mor modifier callbacks
+ * @param mixed $modifier
+ * @return \merry\Config
+ */
+ public function apply($modifier) {
+ if (is_callable($modifier)) {
+ foreach ($this->props as $prop => $value) {
+ $this->__set($prop, $modifier($value, $prop));
+ }
+ } else {
+ foreach ($modifier as $key => $mod) {
+ if (is_callable($mod)) {
+ $this->props->$key = $mod(isset($this->props->$key) ? $this->props->$key : null, $key);
+ } elseif (is_array($mod)) {
+ $this->props->$key->apply($mod);
+ } else {
+ /* */
+ }
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Return the complete config as array
+ * @return array
+ */
+ function toArray() {
+ return $this->arrayify($this);
+ }
+
+ /**
+ * @ignore
+ */
+ function __get($prop) {
+ return $this->props->$prop;
+ }
+
+ /**
+ * @ignore
+ */
+ function __set($prop, $value) {
+ if (isset($value) && !is_scalar($value) && !($value instanceof Config)) {
+ $value = new static((array) $value);
+ }
+ if (!strlen($prop)) {
+ $prop = $this->index++;
+ } elseif (is_numeric($prop) && !strcmp($prop, (int) $prop)) {
+ /* update internal index */
+ if ($prop >= $this->index) {
+ $this->index = $prop + 1;
+ }
+ }
+
+ $this->props->$prop = $value;
+ }
+
+ /**
+ * @ignore
+ */
+ function __isset($prop) {
+ return isset($this->props->$prop);
+ }
+
+ /**
+ * @ignore
+ */
+ function __unset($prop) {
+ unset($this->props->$prop);
+ }
+
+ /**
+ * @ignore
+ */
+ function offsetGet($o) {
+ return $this->props->$o;
+ }
+
+ /**
+ * @ignore
+ */
+ function offsetSet($o, $v) {
+ $this->__set($o, $v);
+ }
+
+ /**
+ * @ignore
+ */
+ function offsetExists($o) {
+ return isset($this->props->$o);
+ }
+
+ /**
+ * @ignore
+ */
+ function offsetUnset($o) {
+ unset($this->props->$o);
+ }
+
+ /**
+ * @ignore
+ */
+ function rewind() {
+ $this->riter = (array) $this->props;
+ reset($this->riter);
+ }
+
+ /**
+ * @ignore
+ */
+ function valid() {
+ return NULL !== key($this->riter);
+ }
+
+ /**
+ * @ignore
+ */
+ function next() {
+ next($this->riter);
+ }
+
+ /**
+ * @ignore
+ */
+ function key() {
+ return key($this->riter);
+ }
+
+ /**
+ * @ignore
+ */
+ function current() {
+ return current($this->riter);
+ }
+
+ /**
+ * @ignore
+ */
+ function hasChildren() {
+ return current($this->riter) instanceof Config;
+ }
+
+ /**
+ * @ignore
+ */
+ function getChildren() {
+ return current($this->riter);
+ }
+}
--- /dev/null
+<?php
+
+namespace merry;
+
+require __DIR__."/../../vendor/autoload.php";
+
+/**
+ * @covers merry\Config
+ */
+class ConfigTest extends \PHPUnit_Framework_TestCase {
+
+ public function testBasic() {
+ $config = ["foo" => "bar", "bar" => "foo"];
+ $object = new Config($config);
+ $this->assertEquals($config, $object->toArray());
+ $this->assertEquals("bar", $object->foo);
+ $this->assertEquals("foo", $object->bar);
+ $this->assertTrue(isset($object->foo));
+ $this->assertFalse(isset($object->foobar));
+ unset($object->bar);
+ $this->assertFalse(isset($object->bar));
+ }
+
+ public function testBasicOffset() {
+ $config = ["foo" => "bar", "bar" => "foo"];
+ $object = new Config($config);
+ $this->assertEquals("bar", $object["foo"]);
+ $this->assertEquals("foo", $object["bar"]);
+ $this->assertTrue(isset($object["foo"]));
+ $this->assertFalse(isset($object["foobar"]));
+ unset($object["bar"]);
+ $this->assertFalse(isset($object["bar"]));
+ }
+
+ public function testBasicSection() {
+ $config = [
+ "primary" => [
+ "foo" => "bar",
+ "bar" => "foo",
+ ],
+ "secondary : primary" => [
+ "bar" => "foo2",
+ "baz" => "sec"
+ ],
+ "tertiary : secondary" => [
+ "bar" => "foo3",
+ "bat" => "tri"
+ ],
+ "alternative : primary" => [
+ "bar" => "fux",
+ "baz" => "alt"
+ ]
+ ];
+ $object = new Config($config, "primary");
+ $this->assertEquals($config["primary"], $object->toArray());
+
+ $object = new Config($config, "secondary");
+ $this->assertEquals($config["secondary : primary"] + $config["primary"], $object->toArray());
+
+ $object = new Config($config, "tertiary");
+ $this->assertEquals($config["tertiary : secondary"] + $config["secondary : primary"] + $config["primary"], $object->toArray());
+
+ $object = new Config($config, "alternative");
+ $this->assertEquals($config["alternative : primary"] + $config["primary"], $object->toArray());
+ }
+
+ public function testSetArray() {
+ $config = ["foo" => "bar", "arr" => [1,2,3]];
+ $object = new Config($config);
+ $object["foo"] = [$object->foo, "baz"];
+ $object["arr"][] = 4;
+
+ $this->assertEquals(["bar", "baz"], $object["foo"]->toArray());
+ $this->assertEquals([1,2,3,4], $object["arr"]->toArray());
+
+ $this->assertEquals(["foo"=>["bar","baz"], "arr"=>[1,2,3,4]], $object->toArray());
+ }
+
+ public function testApply() {
+ $config = [
+ "level1" => [
+ "level2" => [
+ "level3" => "123"
+ ],
+ "level2-1" => [
+ "level3-1" => "321"
+ ]
+ ]
+ ];
+ $object = new Config($config);
+ $this->assertEquals("123", $object->level1["level2"]->level3);
+ $reverse = function ($v){return strrev($v);};
+ $object->apply([
+ "level1" => [
+ "level2" => [
+ "level3" => $reverse
+ ],
+ "level2-1" => [
+ "level3-1" => $reverse
+ ]
+ ]
+ ]);
+ $compare = [
+ "level1" => [
+ "level2" => [
+ "level3" => "321"
+ ],
+ "level2-1" => [
+ "level3-1" => "123"
+ ]
+ ]
+ ];
+ $this->assertEquals($compare, $object->toArray());
+
+ $object->apply(function() {
+ return null;
+ });
+ $this->assertEquals(["level1" => null], $object->toArray());
+ }
+
+ public function testIterator() {
+ $config = [
+ "level1-0" => [
+ "level2-0" => "1-0.2-0",
+ "level2-1" => "1-0.2-1",
+ "level2-2" => [
+ "level3" => "1-0.2-2.3"
+ ]
+ ],
+ "level1-1" => [
+ 1,2,3
+ ]
+ ];
+ $object = new Config($config);
+ $array = [];
+ foreach (new \RecursiveIteratorIterator($object) as $key => $val) {
+ $array[$key] = $val;
+ }
+ $compare = [
+ 'level2-0' => '1-0.2-0',
+ 'level2-1' => '1-0.2-1',
+ 'level3' => '1-0.2-2.3',
+ 1, 2, 3
+ ];
+ $this->assertEquals($compare, $array);
+ }
+}