Skip to content

Commit

Permalink
Create "Attribute" rule
Browse files Browse the repository at this point in the history
With this change, any rule can be used as a PHP attribute. I have wanted
to implement this feature for a while, as it allows you to bind the
validation to a specific property and just validate the object
afterwards.
  • Loading branch information
henriquemoody committed Dec 13, 2024
1 parent 1093ab3 commit 7cec227
Show file tree
Hide file tree
Showing 180 changed files with 720 additions and 25 deletions.
5 changes: 3 additions & 2 deletions bin/create-mixin
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,18 @@ function overwriteFile(string $content, string $basename): void
'Property',
'PropertyExists',
'PropertyOptional',
'Attributes',
];

$mixins = [
['Key', 'key', [], $structureRelatedRules],
['Length', 'length', $numberRelatedRules, []],
['Max', 'max', $numberRelatedRules, []],
['Min', 'min', $numberRelatedRules, []],
['Not', 'not', [], ['Not', 'NotEmpty', 'NotBlank', 'NotEmoji', 'NotUndef', 'NotOptional', 'NullOr', 'UndefOr', 'Optional']],
['Not', 'not', [], ['Not', 'NotEmpty', 'NotBlank', 'NotEmoji', 'NotUndef', 'NotOptional', 'NullOr', 'UndefOr', 'Optional', 'Attributes']],
['NullOr', 'nullOr', [], ['Nullable', 'NullOr', 'Optional', 'NotOptional', 'NotUndef', 'UndefOr']],
['Property', 'property', [], $structureRelatedRules],
['UndefOr', 'undefOr', [], ['Nullable', 'NullOr', 'NotOptional', 'NotUndef', 'Optional', 'UndefOr']],
['UndefOr', 'undefOr', [], ['Nullable', 'NullOr', 'NotOptional', 'NotUndef', 'Optional', 'UndefOr', 'Attributes']],
['Validator', null, [], []],
];

Expand Down
29 changes: 16 additions & 13 deletions docs/02-feature-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ The `assert()` method throws an exception when validation fails. You can handle
v::intType()->positive()->assert($input);
```

## Smart validation

Respect\Validation offers over 150 rules, many of which are designed to address common scenarios. Here’s a quick guide to some specific use cases and the rules that make validation straightforward.

* Using rules as **PHP Attributes**: [Attributes](rules/Attributes.md).
* Validating **Arrays**: [Key](rules/Key.md), [KeyOptional](rules/KeyOptional.md), [KeyExists](rules/KeyExists.md).
* Validating **Array structures**: [KeySet](rules/KeySet.md).
* Validating **Object properties**: [Property](rules/Property.md), [PropertyOptional](rules/PropertyOptional.md), [PropertyExists](rules/PropertyExists.md).
* Using **Conditional validation**: [NullOr](rules/NullOr.md), [UndefOr](rules/UndefOr.md), [When](rules/When.md).
* Using **Grouped validation**: [AllOf](rules/AllOf.md), [AnyOf](rules/AnyOf.md), [NoneOf](rules/NoneOf.md), [OneOf](rules/OneOf.md)
* Validating **Each** value in the input: [Each](rules/Each.md).
* Validating the **Length** of the input: [Length](rules/Length.md).
* Validating the **Maximum** value in the input: [Max](rules/Max.md).
* Validating the **Minimum** value in the input: [Min](rules/Min.md).
* Handling **Special cases**: [Lazy](rules/Lazy.md), [Consecutive](rules/Consecutive.md), [Call](rules/Call.md).

### Custom templates

Define your own error message when the validation fails:
Expand Down Expand Up @@ -101,16 +117,3 @@ v::dateTime('Y-m-d')
->setName('Age')
->assert($input);
```

## Smart input handling

Respect\Validation offers over 150 rules, many of which are designed to address common input handling scenarios. Here’s a quick guide to some specific use cases and the rules that make validation straightforward.

* Validating arrays: [Key](rules/Key.md), [KeyOptional](rules/KeyOptional.md), and [KeyExists](rules/KeyExists.md).
* Validating array structures: [KeySet](rules/KeySet.md).
* Validating object properties: [Property](rules/Property.md), [PropertyOptional](rules/PropertyOptional.md), and [PropertyExists](rules/PropertyExists.md).
* Validating only when input is not `null`: [NullOr](rules/NullOr.md).
* Validating only when input is not `null` or an empty string: [UndefOr](rules/UndefOr.md).
* Validating the length of the input: [Length](rules/Length.md).
* Validating the maximum value of the input: [Max](rules/Max.md).
* Validating the minimum value of the input: [Min](rules/Min.md).
3 changes: 3 additions & 0 deletions docs/09-list-of-rules-by-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@

## Objects

- [Attributes](rules/Attributes.md)
- [Instance](rules/Instance.md)
- [ObjectType](rules/ObjectType.md)
- [Property](rules/Property.md)
Expand Down Expand Up @@ -248,6 +249,7 @@

## Structures

- [Attributes](rules/Attributes.md)
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyOptional](rules/KeyOptional.md)
Expand Down Expand Up @@ -297,6 +299,7 @@
- [AnyOf](rules/AnyOf.md)
- [ArrayType](rules/ArrayType.md)
- [ArrayVal](rules/ArrayVal.md)
- [Attributes](rules/Attributes.md)
- [Base](rules/Base.md)
- [Base64](rules/Base64.md)
- [Between](rules/Between.md)
Expand Down
85 changes: 85 additions & 0 deletions docs/rules/Attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Attributes

- `Attributes()`

Validates the PHP attributes defined in the properties of the input.

Example of object:

```php
use Respect\Validation\Rules;

final class Person
{
public function __construct(
#[Rules\NotEmpty]
public readonly string $name,
#[Rules\Email]
public readonly string $email,
#[Rules\Date('Y-m-d')]
#[Rules\DateTimeDiff('years', new Rules\LessThanOrEqual(25))]
public readonly string $birthdate,
#[Rules\Phone]
public readonly ?string $phone
) {
}
}
```

Here is how you can validate the attributes of the object:

```php
v::attributes()->assert(new Person('John Doe', '[email protected]', '2020-06-23'));
// No exception

v::attributes()->assert(new Person('John Doe', '[email protected]', '2020-06-23', '+31 20 624 1111'));
// No exception

v::attributes()->assert(new Person('', '[email protected]', '2020-06-23', '+1234567890'));
// Message: name must not be empty

v::attributes()->assert(new Person('John Doe', 'not an email', '2020-06-23', '+1234567890'));
// Message: email must be a valid email address

v::attributes()->assert(new Person('John Doe', '[email protected]', 'not a date', '+1234567890'));
// Message: birthdate must be a valid date in the format "2005-12-30"

v::attributes()->assert(new Person('John Doe', '[email protected]', '2020-06-23', 'not a phone number'));
// Message: phone must be a valid telephone number or must be null

v::attributes()->assert(new Person('', 'not an email', 'not a date', 'not a phone number'));
// Full message:
// - All of the required rules must pass for `Person { +$name="" +$email="not an email" +$birthdate="not a date" +$phone="not a phone number" }`
// - name must not be empty
// - email must be a valid email address
// - All of the required rules must pass for birthdate
// - birthdate must be a valid date in the format "2005-12-30"
// - For comparison with now, birthdate must be a valid datetime
// - phone must be a valid telephone number or must be null
```

## Caveats

* If the object has no attributes, the validation will always pass.
* When the property is nullable, this rule will wrap the rule on the property into [NullOr](NullOr.md) rule.
* This rule has no templates because it uses the templates of the rules that are applied to the properties.

## Categorization

- Objects
- Structures

## Changelog

| Version | Description |
|--------:|-------------|
| 3.0.0 | Created |

***
See also:

- [NullOr](NullOr.md)
- [ObjectType](ObjectType.md)
- [Property](Property.md)
- [PropertyExists](PropertyExists.md)
- [PropertyOptional](PropertyOptional.md)
2 changes: 1 addition & 1 deletion docs/rules/Date.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[]()# Date
# Date

- `Date()`
- `Date(string $format)`
Expand Down
1 change: 1 addition & 0 deletions docs/rules/NullOr.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ v::not(v::nullOr(v::alpha()))->assert("alpha");
***
See also:

- [Attributes](Attributes.md)
- [NullType](NullType.md)
- [UndefOr](UndefOr.md)
1 change: 1 addition & 0 deletions docs/rules/ObjectType.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ v::objectType()->isValid(new stdClass); // true
See also:

- [ArrayType](ArrayType.md)
- [Attributes](Attributes.md)
- [BoolType](BoolType.md)
- [BoolVal](BoolVal.md)
- [CallableType](CallableType.md)
Expand Down
1 change: 1 addition & 0 deletions docs/rules/Property.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ This rule will validate public, private, protected, uninitialised, and static pr
***
See also:

- [Attributes](Attributes.md)
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyOptional](KeyOptional.md)
Expand Down
1 change: 1 addition & 0 deletions docs/rules/PropertyExists.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ This rule will validate public, private, protected, uninitialised, and static pr
***
See also:

- [Attributes](Attributes.md)
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyOptional](KeyOptional.md)
Expand Down
1 change: 1 addition & 0 deletions docs/rules/PropertyOptional.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ v::objectType()->propertyOptional('name', v::notEmpty())->isValid('Not an object
***
See also:

- [Attributes](Attributes.md)
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyOptional](KeyOptional.md)
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/ChainedNullOr.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public function nullOrArrayType(): ChainedValidator;

public function nullOrArrayVal(): ChainedValidator;

public function nullOrAttributes(): ChainedValidator;

public function nullOrBase(
int $base,
string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/ChainedValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public function arrayType(): ChainedValidator;

public function arrayVal(): ChainedValidator;

public function attributes(): ChainedValidator;

public function base(
int $base,
string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/StaticNullOr.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public static function nullOrArrayType(): ChainedValidator;

public static function nullOrArrayVal(): ChainedValidator;

public static function nullOrAttributes(): ChainedValidator;

public static function nullOrBase(
int $base,
string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/StaticValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public static function arrayType(): ChainedValidator;

public static function arrayVal(): ChainedValidator;

public static function attributes(): ChainedValidator;

public static function base(
int $base,
string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/AllOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
Expand All @@ -19,6 +20,7 @@
use function array_reduce;
use function count;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'These rules must pass for {{name}}',
'These rules must not pass for {{name}}',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/Alnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Rules\Core\FilteredString;

use function ctype_alnum;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must contain only letters (a-z) and digits (0-9)',
'{{name}} must not contain letters (a-z) or digits (0-9)',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/Alpha.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Rules\Core\FilteredString;

use function ctype_alpha;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must contain only letters (a-z)',
'{{name}} must not contain letters (a-z)',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/AlwaysInvalid.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Rules\Core\Simple;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must be valid',
'{{name}} must be invalid',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/AlwaysValid.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Rules\Core\Simple;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must be valid',
'{{name}} must be invalid',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/AnyOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
Expand All @@ -17,6 +18,7 @@
use function array_map;
use function array_reduce;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'At least one of these rules must pass for {{name}}',
'At least one of these rules must not pass for {{name}}',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

namespace Respect\Validation\Rules;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Rules\Core\Simple;

use function is_array;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must be an array',
'{{name}} must not be an array',
Expand Down
2 changes: 2 additions & 0 deletions library/Rules/ArrayVal.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
namespace Respect\Validation\Rules;

use ArrayAccess;
use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Rules\Core\Simple;
use SimpleXMLElement;

use function is_array;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must be an array value',
'{{name}} must not be an array value',
Expand Down
Loading

0 comments on commit 7cec227

Please sign in to comment.