Remove "KeyNested" rule

Because we have the Key and Property rules, the KeyNested is redundant,
although it's a helpful shortcut.

The real problem is dealing with messages and templates because the
structure of the validator needs to match the structure of the rule.
When using the `getMessages()` method from the exception we throw in
`assert()`, we get a flat structure, which is often not the intended
structure.

The KeyNested rule is cool, but it adds some complexity to the codebase
that I'm unwilling to deal with. It's not nice to remove a rule,
especially because I know people use it, but it's for the best. I'm
trying to keep the codebase small, so hopefully, it will get easier to
maintain.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-03-05 01:59:19 +01:00
parent d36572cc25
commit 24885e4a5f
No known key found for this signature in database
GPG key ID: 221E9281655813A6
13 changed files with 25 additions and 608 deletions

View file

@ -11,7 +11,6 @@
- [In](rules/In.md)
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyNested](rules/KeyNested.md)
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [Sorted](rules/Sorted.md)
@ -163,7 +162,6 @@
- [Call](rules/Call.md)
- [Each](rules/Each.md)
- [Key](rules/Key.md)
- [KeyNested](rules/KeyNested.md)
- [KeySet](rules/KeySet.md)
- [LazyConsecutive](rules/LazyConsecutive.md)
- [NoneOf](rules/NoneOf.md)
@ -248,7 +246,6 @@
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyNested](rules/KeyNested.md)
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [Property](rules/Property.md)
@ -359,7 +356,6 @@
- [Json](rules/Json.md)
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyNested](rules/KeyNested.md)
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [LanguageCode](rules/LanguageCode.md)

View file

@ -12,6 +12,19 @@ v::key('email', v::email())->validate(['email' => 'therespectpanda@gmail.com']);
v::key('age', v::intVal())->validate([]); // false
```
You can also use `Key` to validate nested arrays:
```php
v::key(
'payment_details',
v::key('credit_card', v::creditCard())
)->validate([
'payment_details' => [
'credit_card' => '5376 7473 9720 8720',
],
]); // true
```
The name of this validator is automatically set to the key name.
```php
@ -46,7 +59,6 @@ See also:
- [ArrayVal](ArrayVal.md)
- [Each](Each.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [KeySet](KeySet.md)
- [Property](Property.md)

View file

@ -1,53 +0,0 @@
# KeyNested
- `KeyNested(string $name)`
- `KeyNested(string $name, Validatable $rule)`
- `KeyNested(string $name, Validatable $rule, bool $mandatory)`
Validates an array key or an object property using `.` to represent nested data.
Validating keys from arrays or `ArrayAccess` instances:
```php
$array = [
'foo' => [
'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][].
## Categorization
- Arrays
- Nesting
- Structures
## Changelog
Version | Description
--------|-------------
1.0.0 | Created
***
See also:
- [Key](Key.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)
[Yii2 ArrayHelper]: https://github.com/yiisoft/yii2/blob/68c30c1/framework/helpers/BaseArrayHelper.php "Yii2 ArrayHelper"

View file

@ -16,6 +16,18 @@ v::property('email', v::email())->validate($object); // true
v::property('email', v::email()->endsWith('@example.com'))->assert($object); // false
```
You can also use `Property` to validate nested objects:
```php
$object->address = new stdClass();
$object->address->postalCode = '1017 BS';
v::property(
'address',
v::property('postalCode', v::postalCode('NL'))
)->validate($object); // true
```
The name of this validator is automatically set to the property name.
```php
@ -51,7 +63,6 @@ See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)
- [PropertyExists](PropertyExists.md)

View file

@ -37,7 +37,6 @@ See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)
- [Property](Property.md)

View file

@ -56,7 +56,6 @@ See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)
- [Property](Property.md)

View file

@ -170,12 +170,6 @@ interface ChainedValidator extends Validatable
public function keyOptional(int|string $key, Validatable $rule): ChainedValidator;
public function keyNested(
string $reference,
?Validatable $referenceValidator = null,
bool $mandatory = true
): ChainedValidator;
public function keySet(Validatable $rule, Validatable ...$rules): ChainedValidator;
public function lazyConsecutive(callable $ruleCreator, callable ...$ruleCreators): ChainedValidator;

View file

@ -172,12 +172,6 @@ interface StaticValidator
public static function keyOptional(int|string $key, Validatable $rule): ChainedValidator;
public static function keyNested(
string $reference,
?Validatable $referenceValidator = null,
bool $mandatory = true
): ChainedValidator;
public static function keySet(Validatable $rule, Validatable ...$rules): ChainedValidator;
public static function lazyConsecutive(callable $ruleCreator, callable ...$ruleCreators): ChainedValidator;

View file

@ -1,129 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Result;
use Respect\Validation\Validatable;
abstract class AbstractRelated extends AbstractRule
{
public const TEMPLATE_NOT_PRESENT = '__not_present__';
public const TEMPLATE_INVALID = '__invalid__';
abstract public function hasReference(mixed $input): bool;
abstract public function getReferenceValue(mixed $input): mixed;
public function __construct(
private readonly mixed $reference,
private readonly ?Validatable $rule = null,
private readonly bool $mandatory = true
) {
$this->setName($rule?->getName() ?? (string) $reference);
}
public function evaluate(mixed $input): Result
{
$name = $this->getName() ?? (string) $this->reference;
$hasReference = $this->hasReference($input);
if ($this->mandatory && !$hasReference) {
return Result::failed($input, $this, [], self::TEMPLATE_NOT_PRESENT)->withNameIfMissing($name);
}
if ($this->rule === null || !$hasReference) {
return Result::passed($input, $this, [], self::TEMPLATE_NOT_PRESENT)->withNameIfMissing($name);
}
$result = $this->rule->evaluate($this->getReferenceValue($input));
return (new Result($result->isValid, $input, $this, [], self::TEMPLATE_INVALID))
->withChildren($result)
->withNameIfMissing($name);
}
public function getReference(): mixed
{
return $this->reference;
}
public function isMandatory(): bool
{
return $this->mandatory;
}
public function setName(string $name): static
{
parent::setName($name);
$this->rule?->setName($name);
return $this;
}
public function assert(mixed $input): void
{
$hasReference = $this->hasReference($input);
if ($this->mandatory && !$hasReference) {
throw $this->reportError($input);
}
if ($this->rule === null || !$hasReference) {
return;
}
try {
$this->rule->assert($this->getReferenceValue($input));
} catch (ValidationException $validationException) {
/** @var NestedValidationException $nestedValidationException */
$nestedValidationException = $this->reportError($input, ['name' => $this->reference]);
$nestedValidationException->addChild($validationException);
throw $nestedValidationException;
}
}
public function check(mixed $input): void
{
$hasReference = $this->hasReference($input);
if ($this->mandatory && !$hasReference) {
throw $this->reportError($input);
}
if ($this->rule === null || !$hasReference) {
return;
}
$this->rule->check($this->getReferenceValue($input));
}
public function validate(mixed $input): bool
{
$hasReference = $this->hasReference($input);
if ($this->mandatory && !$hasReference) {
return false;
}
if ($this->rule === null || !$hasReference) {
return true;
}
return $this->rule->validate($this->getReferenceValue($input));
}
protected function getStandardTemplate(mixed $input): string
{
if ($this->rule === null) {
return self::TEMPLATE_NOT_PRESENT;
}
return $this->hasReference($input) ? self::TEMPLATE_INVALID : self::TEMPLATE_NOT_PRESENT;
}
}

View file

@ -1,133 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use ArrayAccess;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\NonOmissibleValidationException;
use Respect\Validation\Message\Template;
use function array_key_exists;
use function array_shift;
use function explode;
use function is_array;
use function is_null;
use function is_object;
use function is_scalar;
use function property_exists;
use function rtrim;
use function sprintf;
#[ExceptionClass(NonOmissibleValidationException::class)]
#[Template(
'No items were found for key chain {{name}}',
'Items for key chain {{name}} must not be present',
self::TEMPLATE_NOT_PRESENT,
)]
#[Template(
'Key chain {{name}} is not valid',
'Key chain {{name}} must not be valid',
self::TEMPLATE_INVALID,
)]
final class KeyNested extends AbstractRelated
{
public function hasReference(mixed $input): bool
{
try {
$this->getReferenceValue($input);
} catch (ComponentException $cex) {
return false;
}
return true;
}
public function getReferenceValue(mixed $input): mixed
{
if (is_scalar($input)) {
$message = sprintf('Cannot select the %s in the given data', $this->getReference());
throw new ComponentException($message);
}
$keys = $this->getReferencePieces();
$value = $input;
while (!is_null($key = array_shift($keys))) {
$value = $this->getValue($value, $key);
}
return $value;
}
/**
* @return string[]
*/
private function getReferencePieces(): array
{
return explode('.', rtrim((string) $this->getReference(), '.'));
}
/**
* @param mixed[] $array
*/
private function getValueFromArray(array $array, mixed $key): mixed
{
if (!array_key_exists($key, $array)) {
$message = sprintf('Cannot select the key %s from the given array', $this->getReference());
throw new ComponentException($message);
}
return $array[$key];
}
/**
* @param ArrayAccess<mixed, mixed> $array
*/
private function getValueFromArrayAccess(ArrayAccess $array, mixed $key): mixed
{
if (!$array->offsetExists($key)) {
$message = sprintf('Cannot select the key %s from the given array', $this->getReference());
throw new ComponentException($message);
}
return $array->offsetGet($key);
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
*/
private function getValueFromObject(object $object, string $property): mixed
{
if (empty($property) || !property_exists($object, $property)) {
$message = sprintf('Cannot select the property %s from the given object', $this->getReference());
throw new ComponentException($message);
}
return $object->{$property};
}
private function getValue(mixed $value, mixed $key): mixed
{
if (is_array($value)) {
return $this->getValueFromArray($value, $key);
}
if ($value instanceof ArrayAccess) {
return $this->getValueFromArrayAccess($value, $key);
}
if (is_object($value)) {
return $this->getValueFromObject($value, $key);
}
$message = sprintf('Cannot select the property %s from the given data', $this->getReference());
throw new ComponentException($message);
}
}

View file

@ -1,38 +0,0 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
$work = new stdClass();
$work->number = '+61.(03) 4546 5498';
$work->countryCode = 61;
$work->primary = true;
$personal = new stdClass();
$personal->number = '123';
$personal->country = 61;
$personal->primary = false;
$phoneNumbers = new stdClass();
$phoneNumbers->personal = $personal;
$phoneNumbers->work = $work;
$validateThis = ['phoneNumbers' => $phoneNumbers];
exceptionMessage(static function () use ($validateThis): void {
v::create()
->keyNested('phoneNumbers.personal.country', v::intType(), false)
->keyNested('phoneNumbers.personal.number', v::phone(), false)
->keyNested('phoneNumbers.personal.primary', v::boolType(), false)
->keyNested('phoneNumbers.work.country', v::intType(), false)
->keyNested('phoneNumbers.work.number', v::phone(), false)
->keyNested('phoneNumbers.work.primary', v::boolType(), false)
->check($validateThis);
});
?>
--EXPECT--
phoneNumbers.personal.number must be a valid telephone number

View file

@ -1,37 +0,0 @@
--TEST--
keyNested()
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
$array = [
'foo' => [
'bar' => 123,
],
];
$object = new stdClass();
$object->foo = new stdClass();
$object->foo->bar = 42;
var_dump(v::keyNested('foo.bar.baz')->validate(['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));
?>
--EXPECT--
bool(false)
bool(true)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)

View file

@ -1,198 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use ArrayObject;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
use stdClass;
#[Group('rule')]
#[CoversClass(AbstractRelated::class)]
#[CoversClass(KeyNested::class)]
final class KeyNestedTest extends TestCase
{
#[Test]
public function arrayWithPresentKeysWillReturnTrueForFullPathValidator(): void
{
$array = [
'bar' => [
'foo' => [
'baz' => 'hello world!',
],
'foooo' => [
'boooo' => 321,
],
],
];
$rule = new KeyNested('bar.foo.baz');
self::assertTrue($rule->validate($array));
}
#[Test]
public function arrayWithNumericKeysWillReturnTrueForFullPathValidator(): void
{
$array = [
0 => 'Zero, the hero!',
1 => 'One, the gun!',
];
$rule = Stub::pass(1);
$sut = new KeyNested(0, $rule);
$sut->check($array);
self::assertSame([$array[0]], $rule->inputs);
}
#[Test]
public function arrayWithPresentKeysWillReturnTrueForHalfPathValidator(): void
{
$array = [
'bar' => [
'foo' => [
'baz' => 'hello world!',
],
'foooo' => [
'boooo' => 321,
],
],
];
$rule = new KeyNested('bar.foo');
self::assertTrue($rule->validate($array));
}
#[Test]
public function objectWithPresentPropertiesWillReturnTrueForDirtyPathValidator(): void
{
$object = (object) [
'bar' => (object) [
'foo' => (object) [
'baz' => 'hello world!',
],
'foooo' => (object) [
'boooo' => 321,
],
],
];
$rule = new KeyNested('bar.foooo.');
self::assertTrue($rule->validate($object));
}
#[Test]
public function emptyInputMustReturnFalse(): void
{
$rule = new KeyNested('bar.foo.baz');
self::assertFalse($rule->validate(''));
}
#[Test]
public function emptyInputMustNotAssert(): void
{
$rule = new KeyNested('bar.foo.baz');
$this->expectException(ValidationException::class);
$rule->assert('');
}
#[Test]
public function emptyInputMustNotCheck(): void
{
$rule = new KeyNested('bar.foo.baz');
$this->expectException(ValidationException::class);
$rule->check('');
}
#[Test]
public function arrayWithEmptyKeyShouldReturnTrue(): void
{
$rule = new KeyNested('emptyKey');
$input = ['emptyKey' => ''];
self::assertTrue($rule->validate($input));
}
#[Test]
public function arrayWithAbsentKeyShouldThrowNestedKeyException(): void
{
$validator = new KeyNested('bar.bar');
$object = [
'baraaaaaa' => [
'bar' => 'foo',
],
];
$this->expectException(ValidationException::class);
$validator->assert($object);
}
#[Test]
public function notArrayShouldThrowKeyException(): void
{
$validator = new KeyNested('baz.bar');
$object = 123;
$this->expectException(ValidationException::class);
$validator->assert($object);
}
#[Test]
public function extraValidatorShouldValidateKey(): void
{
$subValidator = Stub::pass(1);
$validator = new KeyNested('bar.foo.baz', $subValidator);
$object = [
'bar' => [
'foo' => [
'baz' => 'example',
],
],
];
$validator->assert($object);
self::assertSame([$object['bar']['foo']['baz']], $subValidator->inputs);
}
#[Test]
public function notMandatoryExtraValidatorShouldPassWithAbsentKey(): void
{
$subValidator = Stub::pass(1);
$validator = new KeyNested('bar.rab', $subValidator, false);
$object = new stdClass();
self::assertTrue($validator->validate($object));
}
#[Test]
public function arrayAccessWithPresentKeysWillReturnTrue(): void
{
$arrayAccess = new ArrayObject([
'bar' => [
'foo' => [
'baz' => 'hello world!',
],
'foooo' => [
'boooo' => 321,
],
],
]);
$rule = new KeyNested('bar.foo.baz');
self::assertTrue($rule->validate($arrayAccess));
}
}