Split the "Property" rule

Currently, the Property rule has a third parameter that allows the
validation of the wrapped rule to be optional, meaning that the
validation will only happen if the property exists. That parameter makes
the rule harder to understand at times.

I'm splitting the Property rule into Property, PropertyExists, and
PropertyOptional. That way, it becomes apparent when someone wants only
to validate whether a property exists or if they will validate the value
of the property only when it exists.

I deliberately didn't create an abstract class because those rules are
different enough not to have an abstraction. In fact, I can see myself
deleting the AbstractRelated after I refactor the KeyNested rule.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-03-03 21:17:22 +01:00
parent a647a4737b
commit d36572cc25
No known key found for this signature in database
GPG key ID: 221E9281655813A6
26 changed files with 903 additions and 128 deletions

View file

@ -172,6 +172,7 @@
- [OneOf](rules/OneOf.md)
- [Optional](rules/Optional.md)
- [Property](rules/Property.md)
- [PropertyOptional](rules/PropertyOptional.md)
- [When](rules/When.md)
## Numbers
@ -203,6 +204,8 @@
- [Instance](rules/Instance.md)
- [ObjectType](rules/ObjectType.md)
- [Property](rules/Property.md)
- [PropertyExists](rules/PropertyExists.md)
- [PropertyOptional](rules/PropertyOptional.md)
## Strings
@ -249,6 +252,8 @@
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [Property](rules/Property.md)
- [PropertyExists](rules/PropertyExists.md)
- [PropertyOptional](rules/PropertyOptional.md)
## Transformations
@ -405,6 +410,8 @@
- [PrimeNumber](rules/PrimeNumber.md)
- [Printable](rules/Printable.md)
- [Property](rules/Property.md)
- [PropertyExists](rules/PropertyExists.md)
- [PropertyOptional](rules/PropertyOptional.md)
- [PublicDomainSuffix](rules/PublicDomainSuffix.md)
- [Punct](rules/Punct.md)
- [Readable](rules/Readable.md)

View file

@ -50,3 +50,5 @@ See also:
- [KeyOptional](KeyOptional.md)
- [KeySet](KeySet.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)

View file

@ -40,4 +40,5 @@ See also:
- [Key](Key.md)
- [KeyOptional](KeyOptional.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)

View file

@ -47,5 +47,7 @@ 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

@ -55,6 +55,8 @@ See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)
[array]: https://www.php.net/array
[ArrayAccess]: https://www.php.net/arrayaccess

View file

@ -33,6 +33,8 @@ See also:
- [NullType](NullType.md)
- [Number](Number.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)
- [ResourceType](ResourceType.md)
- [StringType](StringType.md)
- [StringVal](StringVal.md)

View file

@ -1,32 +1,38 @@
# Property
- `Property(string $name)`
- `Property(string $name, Validatable $rule)`
- `Property(string $name, Validatable $rule, bool $mandatory)`
- `Property(string $propertyName, Validatable $rule)`
Validates an object property, even private ones.
Validates an object property against a given rule.
```php
$obj = new stdClass;
$obj->foo = 'bar';
$object = new stdClass;
$object->name = 'The Respect Panda';
$object->email = 'therespectpanda@gmail.com';
v::property('foo')->validate($obj); // true
```
v::property('name', v::equals('The Respect Panda'))->validate($object); // true
You can also validate the property itself:
v::property('email', v::email())->validate($object); // true
```php
v::property('foo', v::equals('bar'))->validate($obj); // true
```
Third parameter makes the property presence optional:
```php
v::property('lorem', v::stringType(), false)->validate($obj); // true
v::property('email', v::email()->endsWith('@example.com'))->assert($object); // false
```
The name of this validator is automatically set to the property name.
```php
v::property('website', v::url())->assert($object);
// message: website must be present
v::property('name', v::uppercase())->assert($object);
// message: name must be uppercase
```
## Note
This rule will validate public, private, protected, uninitialised, and static properties.
* To only validate if a property exists, use [PropertyExists](PropertyExists.md) instead.
* To validate a property against a given rule only if the property exists, use [PropertyOptional](PropertyOptional.md) instead.
## Categorization
- Nesting
@ -35,10 +41,10 @@ The name of this validator is automatically set to the property name.
## Changelog
Version | Description
--------|-------------
3.0.0 | Renamed from `Attribute` to `Property`
0.3.9 | Created
| Version | Description |
| ------: |--------------------------------------------------------------------------------------------------------------------------------------|
| 3.0.0 | Renamed from `Attribute` to `Property`, and split by [PropertyExists](PropertyExists.md) and [PropertyOptional](PropertyOptional.md) |
| 0.3.9 | Created |
***
See also:
@ -48,3 +54,5 @@ See also:
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)

View file

@ -0,0 +1,44 @@
# PropertyExists
- `PropertyExists(string $propertyName)`
Validates if an object property exists.
```php
$object = new stdClass;
$object->name = 'The Respect Panda';
$object->email = 'therespectpanda@gmail.com';
v::propertyExists('name')->validate($object); // true
v::propertyExists('email')->validate($object); // true
v::propertyExists('website')->validate($object); // false
```
## Notes
This rule will validate public, private, protected, uninitialised, and static properties.
* To validate a property against a given rule requiring the property to exist, use [Property](Property.md) instead.
* To validate a property against a given rule only if the property exists, use [PropertyOptional](PropertyOptional.md) instead.
## Categorization
- Objects
- Structures
## Changelog
| Version | Description |
| ------: |--------------------------------------|
| 3.0.0 | Created from [Property](Property.md) |
***
See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)
- [Property](Property.md)
- [PropertyOptional](PropertyOptional.md)

View file

@ -0,0 +1,63 @@
# PropertyOptional
- `PropertyOptional(string $propertyName, Validatable $rule)`
Validates an object property against a given rule only if the property exists.
```php
$object = new stdClass;
$object->name = 'The Respect Panda';
$object->email = 'therespectpanda@gmail.com';
v::propertyOptional('name', v::notEmpty())->validate($object); // true
v::propertyOptional('email', v::email())->validate($object); // true
v::propertyOptional('age', v::intVal())->validate($object); // true
v::propertyOptional('website', v::url())->validate($object); // true
v::propertyOptional('name', v::lowercase())->validate($object); // false
```
The name of this validator is automatically set to the property name.
```php
v::propertyOptional('email', v::endsWith('@example.com'))->assert($object);
// message: email must end with "@example.com"
```
## Note
This rule will validate public, private, protected, uninitialised, and static properties. However, it will pass for
anything that is not an object because it will always pass when it doesn't find a property in the input. If you want to
ensure the input is an object, use [ObjectType](ObjectType.md) with it.
```php
v::propertyOptional('name', v::notEmpty())->validate('Not an object'); // true
v::objectType()->propertyOptional('name', v::notEmpty())->validate('Not an object'); // false
```
* To only validate if a property exists, use [PropertyExists](PropertyExists.md) instead.
* To validate a property against a given rule requiring the property to exist, use [Property](Property.md) instead.
## Categorization
- Nesting
- Objects
- Structures
## Changelog
| Version | Description |
| ------: |--------------------------------------|
| 3.0.0 | Created from [Property](Property.md) |
***
See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)

View file

@ -0,0 +1,23 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Helpers;
use ReflectionObject;
trait CanExtractPropertyValue
{
public function extractPropertyValue(object $input, string $property): mixed
{
$reflectionObject = new ReflectionObject($input);
$reflectionProperty = $reflectionObject->getProperty($property);
return $reflectionProperty->isInitialized($input) ? $reflectionProperty->getValue($input) : null;
}
}

View file

@ -30,11 +30,11 @@ interface ChainedValidator extends Validatable
public function arrayVal(): ChainedValidator;
public function property(
string $reference,
?Validatable $validator = null,
bool $mandatory = true
): ChainedValidator;
public function property(string $propertyName, Validatable $validator): ChainedValidator;
public function propertyExists(string $propertyName): ChainedValidator;
public function propertyOptional(string $propertyName, Validatable $validator): ChainedValidator;
public function base(int $base, ?string $chars = null): ChainedValidator;

View file

@ -30,11 +30,11 @@ interface StaticValidator
public static function arrayVal(): ChainedValidator;
public static function property(
string $reference,
?Validatable $validator = null,
bool $mandatory = true
): ChainedValidator;
public static function property(string $propertyName, Validatable $validator): ChainedValidator;
public static function propertyExists(string $propertyName): ChainedValidator;
public static function propertyOptional(string $propertyName, Validatable $validator): ChainedValidator;
public static function base(int $base, ?string $chars = null): ChainedValidator;

View file

@ -9,45 +9,36 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use ReflectionProperty;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\NonOmissibleValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Helpers\CanExtractPropertyValue;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;
use Respect\Validation\Validatable;
use function is_object;
use function property_exists;
#[ExceptionClass(NonOmissibleValidationException::class)]
#[Template(
'Property {{name}} must be present',
'Property {{name}} must not be present',
self::TEMPLATE_NOT_PRESENT,
)]
#[Template(
'Property {{name}} must be valid',
'Property {{name}} must not validate',
self::TEMPLATE_INVALID,
)]
final class Property extends AbstractRelated
final class Property extends Wrapper
{
public function __construct(string $name, ?Validatable $rule = null, bool $mandatory = true)
{
parent::__construct($name, $rule, $mandatory);
use CanBindEvaluateRule;
use CanExtractPropertyValue;
public function __construct(
private readonly string $propertyName,
Validatable $rule,
) {
$rule->setName($rule->getName() ?? $propertyName);
parent::__construct($rule);
}
public function getReferenceValue(mixed $input): mixed
public function evaluate(mixed $input): Result
{
$propertyMirror = new ReflectionProperty($input, (string) $this->getReference());
if ($propertyMirror->isInitialized($input) === false) {
return null;
$propertyExistsResult = $this->bindEvaluate(new PropertyExists($this->propertyName), $this, $input);
if (!$propertyExistsResult->isValid) {
return $propertyExistsResult;
}
return $propertyMirror->getValue($input);
}
$childResult = $this->rule->evaluate($this->extractPropertyValue($input, $this->propertyName));
public function hasReference(mixed $input): bool
{
return is_object($input) && property_exists($input, (string) $this->getReference());
return (new Result($childResult->isValid, $input, $this))
->withChildren($childResult)
->withNameIfMissing($this->rule->getName() ?? $this->propertyName);
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use ReflectionObject;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Standard;
use function is_object;
#[Template(
'{{name}} must be present',
'{{name}} must not be present',
)]
final class PropertyExists extends Standard
{
public function __construct(
private readonly string $propertyName
) {
}
public function evaluate(mixed $input): Result
{
if (!is_object($input)) {
return Result::failed($input, $this)->withNameIfMissing($this->propertyName);
}
$reflection = new ReflectionObject($input);
return new Result($reflection->hasProperty($this->propertyName), $input, $this, name: $this->propertyName);
}
}

View file

@ -0,0 +1,44 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Helpers\CanExtractPropertyValue;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;
use Respect\Validation\Validatable;
final class PropertyOptional extends Wrapper
{
use CanBindEvaluateRule;
use CanExtractPropertyValue;
public function __construct(
private readonly string $propertyName,
Validatable $rule,
) {
$rule->setName($rule->getName() ?? $propertyName);
parent::__construct($rule);
}
public function evaluate(mixed $input): Result
{
$propertyExistsResult = $this->bindEvaluate(new PropertyExists($this->propertyName), $this, $input);
if (!$propertyExistsResult->isValid) {
return $propertyExistsResult->withInvertedMode();
}
$childResult = $this->rule->evaluate($this->extractPropertyValue($input, $this->propertyName));
return (new Result($childResult->isValid, $input, $this))
->withChildren($childResult)
->withNameIfMissing($this->rule->getName() ?? $this->propertyName);
}
}

View file

@ -27,20 +27,18 @@ exceptionFullMessage(static function (): void {
->property(
'mysql',
v::create()
->property('host', v::stringType(), true)
->property('user', v::stringType(), true)
->property('password', v::stringType(), true)
->property('schema', v::stringType(), true),
true
->property('host', v::stringType())
->property('user', v::stringType())
->property('password', v::stringType())
->property('schema', v::stringType())
)
->property(
'postgresql',
v::create()
->property('host', v::stringType(), true)
->property('user', v::stringType(), true)
->property('password', v::stringType(), true)
->property('schema', v::stringType(), true),
true
->property('host', v::stringType())
->property('user', v::stringType())
->property('password', v::stringType())
->property('schema', v::stringType())
)
->setName('the given data')
->assert($object);

View file

@ -18,9 +18,9 @@ exceptionFullMessage(static function (): void {
?>
--EXPECT--
- All of the required rules must pass for `stdClass { +$author="foo" }`
- Property title must be present
- Property description must be present
- title must be present
- description must be present
- All of the required rules must pass for author
- author must be of type integer
- author must have a length between 1 and 2
- Property user must be present
- user must be present

View file

@ -0,0 +1,167 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
// Simple
'Missing property' => [v::property('foo', v::intType()), new stdClass()],
'Default' => [v::property('foo', v::intType()), (object) ['foo' => 'string']],
'Negative' => [v::not(v::property('foo', v::intType())), (object) ['foo' => 12]],
'Double-negative with missing property' => [
v::not(v::not(v::property('foo', v::intType()))),
new stdClass(),
],
// With custom name
'With wrapped name, missing property' => [
v::property('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'),
new stdClass(),
],
'With wrapped name, default' => [
v::property('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'),
(object) ['foo' => 'string'],
],
'With wrapped name, negative' => [
v::not(v::property('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'))->setName('Not'),
(object) ['foo' => 12],
],
'With wrapper name, default' => [
v::property('foo', v::intType())->setName('Wrapper'),
(object) ['foo' => 'string'],
],
'With wrapper name, missing property' => [
v::property('foo', v::intType())->setName('Wrapper'),
new stdClass(),
],
'With wrapper name, negative' => [
v::not(v::property('foo', v::intType())->setName('Wrapper'))->setName('Not'),
(object) ['foo' => 12],
],
'With "Not" name, negative' => [
v::not(v::property('foo', v::intType()))->setName('Not'),
(object) ['foo' => 12],
],
// With custom template
'With template, default' => [
v::property('foo', v::intType()),
(object) ['foo' => 'string'],
'Particularly precautions perplexing property',
],
'With template, negative' => [
v::not(v::property('foo', v::intType())),
(object) ['foo' => 12],
'Not a prompt prospect of a particularly primitive property',
],
]);
?>
--EXPECT--
Missing property
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
Default
⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
Negative
⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
Double-negative with missing property
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
With wrapped name, missing property
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be present
- Wrapped must be present
[
'Wrapped' => 'Wrapped must be present',
]
With wrapped name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be of type integer
- Wrapped must be of type integer
[
'Wrapped' => 'Wrapped must be of type integer',
]
With wrapped name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be of type integer
- Wrapped must not be of type integer
[
'Wrapped' => 'Wrapped must not be of type integer',
]
With wrapper name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
With wrapper name, missing property
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
With wrapper name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With "Not" name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With template, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Particularly precautions perplexing property
- Particularly precautions perplexing property
[
'foo' => 'Particularly precautions perplexing property',
]
With template, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Not a prompt prospect of a particularly primitive property
- Not a prompt prospect of a particularly primitive property
[
'foo' => 'Not a prompt prospect of a particularly primitive property',
]

View file

@ -0,0 +1,49 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
'Default mode' => [v::propertyExists('foo'), (object) ['bar' => 'baz']],
'Negative mode' => [v::not(v::propertyExists('foo')), (object) ['foo' => 'baz']],
'Custom name' => [v::propertyExists('foo')->setName('Custom name'), (object) ['bar' => 'baz']],
'Custom template' => [v::propertyExists('foo'), (object) ['bar' => 'baz'], 'Custom template for `{{name}}`'],
]);
?>
--EXPECT--
Default mode
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
Negative mode
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be present
- foo must not be present
[
'foo' => 'foo must not be present',
]
Custom name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Custom name must be present
- Custom name must be present
[
'Custom name' => 'Custom name must be present',
]
Custom template
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Custom template for `foo`
- Custom template for `foo`
[
'foo' => 'Custom template for `foo`',
]

View file

@ -0,0 +1,134 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
// Simple
'Default' => [v::propertyOptional('foo', v::intType()), (object) ['foo' => 'string']],
'Negative' => [v::not(v::propertyOptional('foo', v::intType())), (object) ['foo' => 12]],
'Negative with missing property' => [
v::not(v::propertyOptional('foo', v::intType())),
new stdClass(),
],
// With custom name
'With wrapped name, default' => [
v::propertyOptional('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'),
(object) ['foo' => 'string'],
],
'With wrapped name, negative' => [
v::not(v::propertyOptional('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'))->setName('Not'),
(object) ['foo' => 12],
],
'With wrapper name, default' => [
v::propertyOptional('foo', v::intType())->setName('Wrapper'),
(object) ['foo' => 'string'],
],
'With wrapper name, negative' => [
v::not(v::propertyOptional('foo', v::intType())->setName('Wrapper'))->setName('Not'),
(object) ['foo' => 12],
],
'With "Not" name, negative' => [
v::not(v::propertyOptional('foo', v::intType()))->setName('Not'),
(object) ['foo' => 12],
],
// With custom template
'With template, default' => [
v::propertyOptional('foo', v::intType()),
(object) ['foo' => 'string'],
'Proper property planners plan precise property plots',
],
'With template, negative' => [
v::not(v::propertyOptional('foo', v::intType())),
(object) ['foo' => 12],
'Not proving prudent property planning promotes prosperity',
],
]);
?>
--EXPECT--
Default
⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
Negative
⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
Negative with missing property
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
With wrapped name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be of type integer
- Wrapped must be of type integer
[
'Wrapped' => 'Wrapped must be of type integer',
]
With wrapped name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be of type integer
- Wrapped must not be of type integer
[
'Wrapped' => 'Wrapped must not be of type integer',
]
With wrapper name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
With wrapper name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With "Not" name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With template, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Proper property planners plan precise property plots
- Proper property planners plan precise property plots
[
'foo' => 'Proper property planners plan precise property plots',
]
With template, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Not proving prudent property planning promotes prosperity
- Not proving prudent property planning promotes prosperity
[
'foo' => 'Not proving prudent property planning promotes prosperity',
]

View file

@ -11,9 +11,13 @@ namespace Respect\Validation\Test\Stubs;
final class WithProperties
{
public string $public = 'public';
public const PUBLIC_VALUE = 'public';
public const PROTECTED_VALUE = 'protected';
public const PRIVATE_VALUE = 'private';
protected string $protected = 'protected';
public string $public = self::PUBLIC_VALUE;
private string $private = 'private'; // @phpstan-ignore-line
protected string $protected = self::PROTECTED_VALUE;
private string $private = self::PRIVATE_VALUE; // @phpstan-ignore-line
}

View file

@ -0,0 +1,23 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Stubs;
final class WithStaticProperties
{
public const PUBLIC_VALUE = 'public';
public const PROTECTED_VALUE = 'protected';
public const PRIVATE_VALUE = 'private';
public static string $public = self::PUBLIC_VALUE;
protected static string $protected = self::PROTECTED_VALUE;
private static string $private = self::PRIVATE_VALUE; // @phpstan-ignore-line
}

View file

@ -11,6 +11,9 @@ namespace Respect\Validation\Test;
use ArrayObject;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use Respect\Validation\Test\Stubs\WithProperties;
use Respect\Validation\Test\Stubs\WithStaticProperties;
use Respect\Validation\Test\Stubs\WithUninitialized;
use Respect\Validation\Validatable;
use stdClass;
@ -259,4 +262,27 @@ abstract class TestCase extends PHPUnitTestCase
'string key with false for a value' => ['foo', ['foo' => false]],
];
}
/** @return array<array{string, object}> */
public static function providerForObjectsWithMissingProperties(): array
{
return [
'object with no properties' => ['something', new stdClass()],
'object with missing property' => ['nonExisting', new WithProperties()],
];
}
/** @return array<array{string, object}> */
public static function providerForObjectsWithExistingProperties(): array
{
return [
'public' => ['public', new WithProperties()],
'protected' => ['protected', new WithProperties()],
'private' => ['private', new WithProperties()],
'uninitialized' => ['uninitialized', new WithUninitialized()],
'static public' => ['public', new WithStaticProperties()],
'static protected' => ['protected', new WithStaticProperties()],
'static private' => ['private', new WithStaticProperties()],
];
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\TestCase;
#[CoversClass(PropertyExists::class)]
final class PropertyExistsTest extends TestCase
{
#[Test]
#[DataProvider('providerForScalarValues')]
public function itShouldAlwaysInvalidateNonObjectValues(mixed $input): void
{
self::assertInvalidInput(new PropertyExists('foo'), $input);
}
#[Test]
#[DataProvider('providerForObjectsWithExistingProperties')]
public function itShouldValidateExistingProperties(string $propertyName, object $object): void
{
self::assertValidInput(new PropertyExists($propertyName), $object);
}
#[Test]
#[DataProvider('providerForObjectsWithMissingProperties')]
public function itShouldInvalidateMissingProperties(string $propertyName, object $object): void
{
self::assertInvalidInput(new PropertyExists($propertyName), $object);
}
}

View file

@ -0,0 +1,89 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
use stdClass;
#[Group('rule')]
#[CoversClass(PropertyOptional::class)]
final class PropertyOptionalTest extends TestCase
{
#[Test]
#[DataProvider('providerForScalarValues')]
public function itShouldAlwaysValidateNonObjectValues(mixed $input): void
{
self::assertValidInput(new PropertyOptional('foo', Stub::daze()), $input);
}
#[Test]
#[DataProvider('providerForObjectsWithMissingProperties')]
public function itShouldAlwaysValidateMissingProperties(string $propertyName, object $object): void
{
self::assertValidInput(new PropertyOptional($propertyName, Stub::daze()), $object);
}
#[Test]
#[DataProvider('providerForObjectsWithExistingProperties')]
public function itShouldValidateExistingPropertiesWithWrappedRule(string $propertyName, object $object): void
{
self::assertValidInput(new PropertyOptional($propertyName, Stub::pass(1)), $object);
}
#[Test]
#[DataProvider('providerForObjectsWithExistingProperties')]
public function itShouldInvalidateExistingPropertiesWithWrappedRule(string $propertyName, object $object): void
{
self::assertInvalidInput(new PropertyOptional($propertyName, Stub::fail(1)), $object);
}
#[Test]
public function itShouldValidatePropertyWithTheWrappedRule(): void
{
$object = new stdClass();
$object->foo = 'bar';
$wrapped = Stub::pass(1);
$rule = new PropertyOptional('foo', $wrapped);
$rule->evaluate($object);
self::assertEquals([$object->foo], $wrapped->inputs);
}
#[Test]
public function itShouldUpdateWrappedRuleNameWithTheGivenName(): void
{
$property = 'foo';
$wrapped = Stub::daze();
new PropertyOptional($property, $wrapped);
self::assertEquals($property, $wrapped->getName());
}
#[Test]
public function itShouldNotUpdateWrappedRuleNameWithTheGivenNameWhenRuleAlreadyHasName(): void
{
$name = 'bar';
$wrapped = Stub::daze();
$wrapped->setName($name);
new PropertyOptional('foo', $wrapped);
self::assertEquals($name, $wrapped->getName());
}
}

View file

@ -10,64 +10,80 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\RuleTestCase;
use Respect\Validation\Test\Stubs\WithProperties;
use Respect\Validation\Test\Stubs\WithUninitialized;
use Respect\Validation\Test\TestCase;
use stdClass;
#[Group('rule')]
#[CoversClass(AbstractRelated::class)]
#[CoversClass(Property::class)]
final class PropertyTest extends RuleTestCase
final class PropertyTest extends TestCase
{
/** @return iterable<string, array{Property, mixed}> */
public static function providerForValidInput(): iterable
#[Test]
#[DataProvider('providerForScalarValues')]
public function itShouldAlwaysInvalidateNonObjectValues(mixed $input): void
{
return [
'attribute is present without extra validator' => [new Property('public'), new WithProperties()],
'private attribute is present without extra validator' => [
new Property('private'),
new WithProperties(),
],
'attribute is present with extra validator' => [
new Property('public', Stub::pass(1)),
new WithProperties(),
],
'attribute is present but uninitialized' => [
new Property('uninitialized'),
new WithUninitialized(),
],
'non mandatory attribute is not present' => [
new Property('nonexistent', null, false),
new WithProperties(),
],
'non mandatory attribute is not present with extra validator' => [
new Property('nonexistent', Stub::daze(), false),
new WithProperties(),
],
'attribute is present but uninitialized with extra validator' => [
new Property('uninitialized', Stub::pass(1)),
new WithUninitialized(),
],
];
self::assertInvalidInput(new Property('foo', Stub::daze()), $input);
}
/** @return iterable<string, array{Property, mixed}> */
public static function providerForInvalidInput(): iterable
#[Test]
#[DataProvider('providerForObjectsWithMissingProperties')]
public function itShouldAlwaysInvalidateMissingProperties(string $propertyName, object $object): void
{
return [
'attribute is absent without extra validator' => [new Property('barr'), new WithProperties()],
'attribute is absent with extra validator' => [new Property('barr', Stub::daze()), new WithProperties()],
'private attribute is not valid based on extra validator' => [
new Property('private', Stub::fail(1)),
new WithProperties(),
],
'value provided is an empty string' => [new Property('barr'), ''],
'validator related to attribute does not validate' => [
new Property('public', Stub::fail(1)),
new WithProperties(),
],
];
self::assertInvalidInput(new Property($propertyName, Stub::fail(1)), $object);
}
#[Test]
#[DataProvider('providerForObjectsWithExistingProperties')]
public function itShouldValidateExistingPropertiesWithWrappedRule(string $propertyName, object $object): void
{
self::assertValidInput(new Property($propertyName, Stub::pass(1)), $object);
}
#[Test]
#[DataProvider('providerForObjectsWithExistingProperties')]
public function itShouldInvalidateExistingPropertiesWithWrappedRule(string $propertyName, object $object): void
{
self::assertInvalidInput(new Property($propertyName, Stub::fail(1)), $object);
}
#[Test]
public function itShouldValidatePropertyWithTheWrappedRule(): void
{
$object = new stdClass();
$object->foo = 'bar';
$wrapped = Stub::pass(1);
$rule = new Property('foo', $wrapped);
$rule->evaluate($object);
self::assertEquals([$object->foo], $wrapped->inputs);
}
#[Test]
public function itShouldUpdateWrappedRuleNameWithTheGivenName(): void
{
$property = 'foo';
$wrapped = Stub::daze();
new Property($property, $wrapped);
self::assertEquals($property, $wrapped->getName());
}
#[Test]
public function itShouldNotUpdateWrappedRuleNameWithTheGivenNameWhenRuleAlreadyHasName(): void
{
$name = 'bar';
$wrapped = Stub::daze();
$wrapped->setName($name);
new Property('foo', $wrapped);
self::assertEquals($name, $wrapped->getName());
}
}