initial checkin
authorMichael Wallner <mike@php.net>
Thu, 14 Aug 2014 17:35:09 +0000 (19:35 +0200)
committerMichael Wallner <mike@php.net>
Thu, 14 Aug 2014 17:35:09 +0000 (19:35 +0200)
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
composer.json [new file with mode: 0644]
lib/merry/Config.php [new file with mode: 0644]
tests/merry/ConfigTest.php [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..aab7eee
--- /dev/null
@@ -0,0 +1,2 @@
+nbproject
+vendor
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..233da7a
--- /dev/null
@@ -0,0 +1,10 @@
+language: php
+
+php:
+    - 5.5
+    - 5.6
+    - hhvm
+
+before_script: composer install
+    
+script: phpunit --coverage-text --color tests
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..0ec221c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+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.
+
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..7710cb2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,80 @@
+= 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
diff --git a/composer.json b/composer.json
new file mode 100644 (file)
index 0000000..1aa36da
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "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"
+        }
+    }
+}
diff --git a/lib/merry/Config.php b/lib/merry/Config.php
new file mode 100644 (file)
index 0000000..a00c976
--- /dev/null
@@ -0,0 +1,271 @@
+<?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);
+       }
+}
diff --git a/tests/merry/ConfigTest.php b/tests/merry/ConfigTest.php
new file mode 100644 (file)
index 0000000..4a11dfb
--- /dev/null
@@ -0,0 +1,147 @@
+<?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);
+       }
+}