diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a10fb2..5abaf889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes of the Respect\Validation releases are documented in this fi - Create "Factor" rule (#405) - Create "Finite" rule (#397) - Create "Infinite" rule (#397) +- Create "KeyNested" rule (#429) - Create "KeySet" rule (#374) - Create "Mimetype" rule (#361) - Create "Optional" rule (#423) diff --git a/docs/KeyNested.md b/docs/KeyNested.md new file mode 100644 index 00000000..52c17bae --- /dev/null +++ b/docs/KeyNested.md @@ -0,0 +1,40 @@ +# KeyNested + +- `v::keyNested(string $name)` +- `v::keyNested(string $name, v $validator)` +- `v::keyNested(string $name, v $validator, boolean $mandatory = true)` + +Validates an array key or an object property using `.` to represent nested data. + +Validating keys from arrays or `ArrayAccess` instances: + +```php +$array = array( + 'foo' => array( + 'bar' => 123, + ), +); + +v::keyNested('foo.bar')->validate($array); // true +``` + +Validating object properties: + +```php +$object = new stdClass(); +$object->foo = new stdClass(); +$object->foo->bar = 42; + +v::keyNested('foo.bar')->validate($object); // true +``` + +This rule was inspired by [Yii2 ArrayHelper][]. + +*** +See also: + + * [Attribute](Attribute.md) + * [Key](Key.md) + + +[Yii2 ArrayHelper]: https://github.com/yiisoft/yii2/blob/68c30c1/framework/helpers/BaseArrayHelper.php "Yii2 ArrayHelper" diff --git a/docs/VALIDATORS.md b/docs/VALIDATORS.md index 7e5dba7e..9de959a4 100644 --- a/docs/VALIDATORS.md +++ b/docs/VALIDATORS.md @@ -98,6 +98,7 @@ * [EndsWith](EndsWith.md) * [In](In.md) * [Key](Key.md) + * [KeyNested](KeySet.md) * [KeySet](KeySet.md) * [Length](Length.md) * [NotEmpty](NotEmpty.md) @@ -228,6 +229,7 @@ * [Ip](Ip.md) * [Json](Json.md) * [Key](Key.md) + * [KeyNested](KeyNested.md) * [KeySet](KeySet.md) * [LeapDate](LeapDate.md) * [LeapYear](LeapYear.md) diff --git a/library/Exceptions/KeyNestedException.php b/library/Exceptions/KeyNestedException.php new file mode 100644 index 00000000..4a26217d --- /dev/null +++ b/library/Exceptions/KeyNestedException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the "LICENSE.md" + * file that was distributed with this source code. + */ + +namespace Respect\Validation\Exceptions; + +class KeyNestedException extends AttributeException +{ + public static $defaultTemplates = array( + self::MODE_DEFAULT => array( + self::NOT_PRESENT => 'No items were found for key chain {{name}}', + self::INVALID => 'Key chain {{name}} is not valid', + ), + self::MODE_NEGATIVE => array( + self::NOT_PRESENT => 'Items for key chain {{name}} must not be present', + self::INVALID => 'Key chain {{name}} must not be valid', + ), + ); +} diff --git a/library/Rules/KeyNested.php b/library/Rules/KeyNested.php new file mode 100644 index 00000000..f91bc1c5 --- /dev/null +++ b/library/Rules/KeyNested.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the "LICENSE.md" + * file that was distributed with this source code. + */ + +namespace Respect\Validation\Rules; + +use ArrayAccess; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Validatable; + +class KeyNested extends AbstractRelated +{ + public function hasReference($input) + { + try { + $this->getReferenceValue($input); + } catch (ComponentException $cex) { + return false; + } + + return true; + } + + private function getReferencePieces() + { + return explode('.', $this->reference); + } + + private function getReferenceArrayValue($input) + { + $keys = $this->getReferencePieces(); + $value = $input; + while (($key = array_shift($keys))) { + if (!isset($value[$key])) { + $message = sprintf('Cannot select the key %s from the given array', $this->reference); + throw new ComponentException($message); + } + + $value = $value[$key]; + } + + return $value; + } + + private function getReferenceObjectValue($input) + { + $properties = $this->getReferencePieces(); + $value = $input; + while (($property = array_shift($properties))) { + if (!isset($value->$property)) { + $message = sprintf('Cannot select the property %s from the given object', $this->reference); + throw new ComponentException($message); + } + + $value = $value->$property; + } + + return $value; + } + + public function getReferenceValue($input) + { + if (is_array($input) || $input instanceof ArrayAccess) { + return $this->getReferenceArrayValue($input); + } + + if (is_object($input)) { + return $this->getReferenceObjectValue($input); + } + + $message = sprintf('Cannot select the %s in the given data', $this->reference); + throw new ComponentException($message); + } +} diff --git a/library/Validator.php b/library/Validator.php index c941d910..a42920c9 100644 --- a/library/Validator.php +++ b/library/Validator.php @@ -70,6 +70,7 @@ use Respect\Validation\Rules\Key; * @method static Validator ip(mixed $ipOptions = null) * @method static Validator json() * @method static Validator key(string $reference, Validatable $referenceValidator = null, bool $mandatory = true) + * @method static Validator keyNested(string $reference, Validatable $referenceValidator = null, bool $mandatory = true) * @method static Validator keySet(Key $rule...) * @method static Validator leapDate(string $format) * @method static Validator leapYear() diff --git a/tests/integration/key-nested.phpt b/tests/integration/key-nested.phpt new file mode 100644 index 00000000..455a4f53 --- /dev/null +++ b/tests/integration/key-nested.phpt @@ -0,0 +1,35 @@ +--TEST-- +alwaysInvalid() +--FILE-- + array( + 'bar' => 123, + ), +); + +$object = new stdClass(); +$object->foo = new stdClass(); +$object->foo->bar = 42; + + +var_dump(v::keyNested('foo.bar.baz')->validate(array('foo.bar.baz' => false))); +var_dump(v::keyNested('foo.bar')->validate($array)); +var_dump(v::keyNested('foo.bar')->validate(new ArrayObject($array))); +var_dump(v::keyNested('foo.bar', v::negative())->validate($array)); +var_dump(v::keyNested('foo.bar')->validate($object)); +var_dump(v::keyNested('foo.bar', v::stringType())->validate($object)); +var_dump(v::keyNested('foo.bar.baz', v::notEmpty(), false)->validate($object)); +?> +--EXPECTF-- +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) diff --git a/tests/unit/Rules/KeyNestedTest.php b/tests/unit/Rules/KeyNestedTest.php new file mode 100644 index 00000000..6ffe192e --- /dev/null +++ b/tests/unit/Rules/KeyNestedTest.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the "LICENSE.md" + * file that was distributed with this source code. + */ + +namespace Respect\Validation\Rules; + +use ArrayObject; + +/** + * @group rule + * @covers Respect\Validation\Rules\KeyNested + * @covers Respect\Validation\Exceptions\KeyNestedException + */ +class KeyNestedTest extends \PHPUnit_Framework_TestCase +{ + public function testArrayWithPresentKeysWillReturnTrueForFullPathValidator() + { + $array = array( + 'bar' => array( + 'foo' => array( + 'baz' => 'hello world!', + ), + 'foooo' => array( + 'boooo' => 321, + ), + ), + ); + + $rule = new KeyNested('bar.foo.baz'); + + $this->assertTrue($rule->validate($array)); + } + + public function testArrayWithPresentKeysWillReturnTrueForHalfPathValidator() + { + $array = array( + 'bar' => array( + 'foo' => array( + 'baz' => 'hello world!', + ), + 'foooo' => array( + 'boooo' => 321, + ), + ), + ); + + $rule = new KeyNested('bar.foo'); + + $this->assertTrue($rule->validate($array)); + } + + public function testOnjectWithPresentPropertiesWillReturnTrueForDirtyPathValidator() + { + $object = (object) array( + 'bar' => (object) array( + 'foo' => (object) array( + 'baz' => 'hello world!', + ), + 'foooo' => (object) array( + 'boooo' => 321, + ), + ), + ); + + $rule = new KeyNested('bar.foooo.'); + + $this->assertTrue($rule->validate($object)); + } + + public function testEmptyInputMustReturnFalse() + { + $rule = new KeyNested('bar.foo.baz'); + + $this->assertFalse($rule->validate('')); + } + + /** + * @expectedException Respect\Validation\Exceptions\KeyNestedException + */ + public function testEmptyInputMustNotAssert() + { + $rule = new KeyNested('bar.foo.baz'); + $rule->assert(''); + } + + /** + * @expectedException Respect\Validation\Exceptions\KeyNestedException + */ + public function testEmptyInputMustNotCheck() + { + $rule = new KeyNested('bar.foo.baz'); + $rule->check(''); + } + + public function testArrayWithEmptyKeyShouldReturnTrue() + { + $rule = new KeyNested('emptyKey'); + $input = array('emptyKey' => ''); + + $this->assertTrue($rule->validate($input)); + } + + /** + * @expectedException Respect\Validation\Exceptions\KeyNestedException + */ + public function testArrayWithAbsentKeyShouldThrowNestedKeyException() + { + $validator = new KeyNested('bar.bar'); + $object = array( + 'baraaaaaa' => array( + 'bar' => 'foo', + ), + ); + $this->assertTrue($validator->assert($object)); + } + + /** + * @expectedException Respect\Validation\Exceptions\KeyNestedException + */ + public function testNotArrayShouldThrowKeyException() + { + $validator = new KeyNested('baz.bar'); + $object = 123; + $this->assertFalse($validator->assert($object)); + } + + public function testExtraValidatorShouldValidateKey() + { + $subValidator = new Length(3, 7); + $validator = new KeyNested('bar.foo.baz', $subValidator); + $object = array( + 'bar' => array( + 'foo' => array( + 'baz' => 'example', + ), + ), + ); + $this->assertTrue($validator->assert($object)); + } + + public function testNotMandatoryExtraValidatorShouldPassWithAbsentKey() + { + $subValidator = new Length(1, 3); + $validator = new KeyNested('bar.rab', $subValidator, false); + $object = new \stdClass(); + $this->assertTrue($validator->validate($object)); + } + + public function testArrayAccessWithPresentKeysWillReturnTrue() + { + $arrayAccess = new ArrayObject(array( + 'bar' => array( + 'foo' => array( + 'baz' => 'hello world!', + ), + 'foooo' => array( + 'boooo' => 321, + ), + ), + )); + + $rule = new KeyNested('bar.foo.baz'); + + $this->assertTrue($rule->validate($arrayAccess)); + } +}