From f1cd126ded6091298b78478df4fdb56109fd45c6 Mon Sep 17 00:00:00 2001 From: David Schoenbauer Date: Thu, 8 Dec 2016 13:52:53 -0600 Subject: [PATCH] Added wildcard to GET resolves: #40 --- src/DotNotation/ArrayDotNotation.php | 110 +++++++++++++++++++-- tests/DotNotation/ArrayDotNotationTest.php | 98 ++++++++++++++---- 2 files changed, 181 insertions(+), 27 deletions(-) diff --git a/src/DotNotation/ArrayDotNotation.php b/src/DotNotation/ArrayDotNotation.php index 5ce0ebf..3caace1 100644 --- a/src/DotNotation/ArrayDotNotation.php +++ b/src/DotNotation/ArrayDotNotation.php @@ -35,16 +35,35 @@ * An easier way to deal with complex PHP arrays * * @author David Schoenbauer - * @version 1.1.1 + * @version 1.3.0 */ class ArrayDotNotation { + const WILD_CARD_CHARACTER = "*"; + + /** + * Returns only the values that match the dot notation path. Used only with wild cards. + */ + const MODE_RETURN_FOUND = 'found'; + + /** + * Returns a default value if the dot notation path is not found. + */ + const MODE_RETURN_DEFAULT = 'default'; + + /** + * Throws a DSchoenbauer\DotNotation\Exception\PathNotFoundException if the path is not found in the data. + */ + const MODE_THROW_EXCEPTION = 'exception'; + /** * Property that houses the data that the dot notation should access * @var array */ private $_data = []; private $_notationType = "."; + private $_defaultValue; + private $_getMode; /** * Sets the data to parse in a chain @@ -65,7 +84,7 @@ public static function with(array $data = []) { * @param array $data optional Array of data that will be accessed via dot notation. */ public function __construct(array $data = []) { - $this->setData($data); + $this->setData($data)->setGetMode(self::MODE_RETURN_DEFAULT); } /** @@ -103,7 +122,8 @@ public function setData(array $data) { * @return mixed value found via dot notation in the array of data */ public function get($dotNotation, $defaultValue = null) { - return $this->recursiveGet($this->getData(), $this->getKeys($dotNotation), $defaultValue); + $this->setDefaultValue($defaultValue); + return $this->recursiveGet($this->getData(), $this->getKeys($dotNotation)); } /** @@ -114,14 +134,32 @@ public function get($dotNotation, $defaultValue = null) { * @param mixed $defaultValue value to return when a key is not found * @return mixed value that the keys find in the data array */ - protected function recursiveGet($data, $keys, $defaultValue) { + protected function recursiveGet($data, $keys) { $key = array_shift($keys); - if (is_array($data) && $key && count($keys) == 0) { //Last Key - return array_key_exists($key, $data) ? $data[$key] : $defaultValue; + if (is_array($data) && $key === static::WILD_CARD_CHARACTER) { + return $this->wildCardGet($keys, $data); + } elseif (is_array($data) && $key && count($keys) == 0) { //Last Key + return array_key_exists($key, $data) ? $data[$key] : $this->getDefaultValue($key); } elseif (is_array($data) && array_key_exists($key, $data)) { - return $this->recursiveGet($data[$key], $keys, $defaultValue); + return $this->recursiveGet($data[$key], $keys); } - return $defaultValue; + return $this->getDefaultValue($key); + } + + protected function wildCardGet(array $keys, $data) { + $output = []; + foreach ($data as $key => $value) { + try { + $tempKeys = $keys; + array_unshift($tempKeys, $key); + $output[] = $this->recursiveGet($data, $tempKeys); + } catch (\Exception $exc) { + if ($this->getGetMode() !== self::MODE_RETURN_FOUND) { + throw $exc; + } + } + } + return $output; } /** @@ -224,6 +262,7 @@ protected function recursiveRemove(array &$data, array $keys) { /** * consistently parses notation keys + * @since 1.2.0 * @param type $notation key path to a value in an array * @return array array of keys as delimited by the notation type */ @@ -234,6 +273,7 @@ protected function getKeys($notation) { /** * Returns the current notation type that delimits the notation path. * Default: "." + * @since 1.2.0 * @return string current notation character delimiting the notation path */ public function getNotationType() { @@ -242,6 +282,7 @@ public function getNotationType() { /** * Sets the current notation type used to delimit the notation path. + * @since 1.2.0 * @param string $notationType * @return $this */ @@ -252,6 +293,8 @@ public function setNotationType($notationType = ".") { /** * Checks to see if a dot notation path is present in the data set. + * + * @since 1.2.0 * @param string $dotNotation dot notation representation of keys of where to remove a value * @return boolean returns true if the dot notation path exists in the data */ @@ -268,4 +311,55 @@ public function has($dotNotation) { return true; } + /** + * Returns the current default value. + * @note should be a protected method + * @since 1.3.0 + * @param type $key + * @return mixed value to be used instead of a real value is not found + * @throws PathNotFoundException when the key + */ + public function getDefaultValue($key = null) { + if ($key !== null && in_array($this->getGetMode(), [self::MODE_THROW_EXCEPTION, self::MODE_RETURN_FOUND])) { + throw new PathNotFoundException($key); + } + return $this->_defaultValue; + } + + /** + * The default value that get will return. Do not set the value with this method. Set the default value in the get method. + * @note should be a protected method + * @since 1.3.0 + * @param mixed $defaultValue value to be used instead of a real value is not found + * @return $this + */ + public function setDefaultValue($defaultValue) { + $this->_defaultValue = $defaultValue; + return $this; + } + + /** + * Returns the behavior defined for how get will return a value. + * @since 1.3.0 + * @return enum values are constants of this class MODE_RETURN_DEFAULT, MODE_RETURN_FOUND, MODE_THROW_EXCEPTION + */ + public function getGetMode() { + return $this->_getMode; + } + + /** + * Defines the behavior on how get returns a value. + * @since 1.3.0 + * @param enum $getMode values are constants of this class MODE_RETURN_DEFAULT, MODE_RETURN_FOUND, MODE_THROW_EXCEPTION + * @return $this + */ + public function setGetMode($getMode) { + $modes = [self::MODE_RETURN_DEFAULT, self::MODE_RETURN_FOUND, self::MODE_THROW_EXCEPTION]; + if (!in_array($getMode, $modes)) { + throw new Exception\InvalidArgumentException('Not a supported mode. Please use on of:' . implode(', ', $modes)); + } + $this->_getMode = $getMode; + return $this; + } + } diff --git a/tests/DotNotation/ArrayDotNotationTest.php b/tests/DotNotation/ArrayDotNotationTest.php index e96444f..d3ee8f1 100644 --- a/tests/DotNotation/ArrayDotNotationTest.php +++ b/tests/DotNotation/ArrayDotNotationTest.php @@ -2,6 +2,8 @@ namespace DSchoenbauer\DotNotation; +use DSchoenbauer\DotNotation\Exception\PathNotArrayException; +use DSchoenbauer\DotNotation\Exception\PathNotFoundException; use DSchoenbauer\DotNotation\Exception\UnexpectedValueException; use PHPUnit_Framework_TestCase; @@ -37,17 +39,17 @@ public function testDataConstructor() { $object = new ArrayDotNotation($data); $this->assertEquals($data, $object->getData()); } - - public function testWith(){ + + public function testWith() { $this->assertInstanceOf(ArrayDotNotation::class, ArrayDotNotation::with()); } - public function testWithData(){ - $data = ['test'=>'value']; + public function testWithData() { + $data = ['test' => 'value']; $this->assertEquals($data, ArrayDotNotation::with($data)->getData()); } - - public function testWithNoData(){ + + public function testWithNoData() { $this->assertEquals([], ArrayDotNotation::with()->getData()); } @@ -55,14 +57,52 @@ public function testGet() { $this->assertEquals('someValueB', $this->_object->get('levelA.levelB')); } + public function testGetWildCardEndWithWild() { + $this->assertEquals(['someValueB'], $this->_object->get('levelA.*')); + } + + public function testGetWildCardDefault() { + $this->assertEquals(['someValueB', null, null], $this->_object->get('*.levelB')); + } + + public function testGetWildCardOnlyFound() { + $this->assertEquals(['someValueB'], $this->_object->setGetMode(ArrayDotNotation::MODE_RETURN_FOUND)->get('*.levelB')); + } + + public function testGetWildTable() { + $data = [ + 'test' => [ + 'test' => [ + ['value' => 'a', 'ontme' => 1], + ['value' => 'b', 'ontme' => 1], + ['value' => 'c', 'ontme' => 1], + ['value' => 'd', 'ontme' => 1], + ['value' => 'e', 'ontme' => 1], + ['value' => 'f', 'ontme' => 1], + ] + ] + ]; + $this->assertEquals(['a', 'b', 'c', 'd', 'e', 'f'], $this->_object->setData($data)->get('test.test.*.value')); + } + + public function testGetWildCardNotFoundException(){ + $this->expectException(PathNotFoundException::class); + $this->_object->setGetMode(ArrayDotNotation::MODE_THROW_EXCEPTION)->get('*.levelC', 'noValue'); + } + public function testGetNoFindDefaultValue() { $this->assertEquals('noValue', $this->_object->get('levelA.levelB.levelC', 'noValue')); } - public function testGetSameLevelDefaultValue() { + public function testGetDefaultValue() { $this->assertEquals('noValue', $this->_object->get('levelA.levelC', 'noValue')); } + public function testGetException() { + $this->expectException(PathNotFoundException::class); + $this->_object->setGetMode(ArrayDotNotation::MODE_THROW_EXCEPTION)->get('levelA.levelC', 'noValue'); + } + public function testSetInitialLevelNewValue() { $this->assertEquals('noValue', $this->_object->get('levelD', 'noValue')); $this->assertEquals('newValue', $this->_object->set('levelD', 'newValue')->get('levelD')); @@ -147,27 +187,22 @@ public function testRemove() { $this->assertEquals($data, $this->_object->remove('levelA.levelB')->getData()); } - /** - * @expectedException \DSchoenbauer\DotNotation\Exception\PathNotFoundException - */ public function testRemovePathNotFound() { + $this->expectException(PathNotFoundException::class); $this->_object->remove('levelA.levelC'); } - - /** - * @expectedException \DSchoenbauer\DotNotation\Exception\PathNotFoundException - */ + public function testRemovePathNotFoundShort() { + $this->expectException(PathNotFoundException::class); $this->_object->remove('level0'); } - /** - * @expectedException \DSchoenbauer\DotNotation\Exception\PathNotArrayException - */ + public function testRemovePathNotArray() { + $this->expectException(PathNotArrayException::class); $this->_object->remove('levelA.levelB.levelD'); } - public function testChangeNotationType(){ + public function testChangeNotationType() { $this->assertEquals('someValueB', $this->_object->setNotationType('-')->get('levelA-levelB')); } @@ -179,7 +214,7 @@ public function testChangeNotationTypeSameLevelDefaultValue() { $this->assertEquals('noValue', $this->_object->setNotationType('-')->get('levelA-levelC', 'noValue')); } - public function testHas(){ + public function testHas() { $this->assertTrue($this->_object->has('levelA')); $this->assertTrue($this->_object->has('levelA.levelB')); $this->assertTrue($this->_object->has('levelB')); @@ -189,4 +224,29 @@ public function testHas(){ $this->assertFalse($this->_object->has('level2')); } + public function testGetMode() { + $this->assertEquals(ArrayDotNotation::MODE_RETURN_DEFAULT, $this->_object->setGetMode(ArrayDotNotation::MODE_RETURN_DEFAULT)->getGetMode()); + $this->assertEquals(ArrayDotNotation::MODE_RETURN_FOUND, $this->_object->setGetMode(ArrayDotNotation::MODE_RETURN_FOUND)->getGetMode()); + $this->assertEquals(ArrayDotNotation::MODE_THROW_EXCEPTION, $this->_object->setGetMode(ArrayDotNotation::MODE_THROW_EXCEPTION)->getGetMode()); + + $this->expectException(Exception\InvalidArgumentException::class); + $this->_object->setGetMode('not a mode'); + } + + public function testDefaultValue() { + $this->assertEquals('test', $this->_object->setDefaultValue('test')->getDefaultValue(), 'Straight pass through'); + $this->assertEquals('test', $this->_object->setDefaultValue('test')->getDefaultValue('key'), 'Key Ignored due to mode'); + + $this->assertEquals('test', $this->_object->setDefaultValue('test')->setGetMode(ArrayDotNotation::MODE_THROW_EXCEPTION)->getDefaultValue(), 'Mode ignored without key'); + $this->assertEquals('test', $this->_object->setDefaultValue('test')->setGetMode(ArrayDotNotation::MODE_RETURN_FOUND)->getDefaultValue(), 'Mode ignored without key'); + + $this->expectException(PathNotFoundException::class); + $this->_object->setDefaultValue('test')->setGetMode(ArrayDotNotation::MODE_THROW_EXCEPTION)->getDefaultValue('key'); + } + + public function testDefaultValueOnlyFound() { + $this->expectException(PathNotFoundException::class); + $this->_object->setDefaultValue('test')->setGetMode(ArrayDotNotation::MODE_RETURN_FOUND)->getDefaultValue('key'); + } + }