From ab50f5e693dac5c9553e8bfc53ef5e1b2aecc26f Mon Sep 17 00:00:00 2001 From: Michael Wallner Date: Thu, 14 Aug 2014 19:35:09 +0200 Subject: [PATCH] initial checkin --- .gitignore | 2 + .travis.yml | 10 ++ LICENSE | 23 ++++ README.md | 80 +++++++++++ composer.json | 19 +++ lib/merry/Config.php | 271 +++++++++++++++++++++++++++++++++++++ tests/merry/ConfigTest.php | 147 ++++++++++++++++++++ 7 files changed, 552 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 lib/merry/Config.php create mode 100644 tests/merry/ConfigTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aab7eee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +nbproject +vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..233da7a --- /dev/null +++ b/.travis.yml @@ -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 index 0000000..0ec221c --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Michael Wallner . +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 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 index 0000000..1aa36da --- /dev/null +++ b/composer.json @@ -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 index 0000000..a00c976 --- /dev/null +++ b/lib/merry/Config.php @@ -0,0 +1,271 @@ + + */ +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 index 0000000..4a11dfb --- /dev/null +++ b/tests/merry/ConfigTest.php @@ -0,0 +1,147 @@ + "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); + } +} -- 2.30.2