Skip to content

Commit 021dde7

Browse files
committed
Add EntityValidationSubscriber, ColumnValidator and inherited attributes
1 parent a1d9f44 commit 021dde7

File tree

14 files changed

+655
-31
lines changed

14 files changed

+655
-31
lines changed

README.md

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
## Entity validator for Doctrine ORM 3
22

3-
By default, entity validation based on attached `\Doctrine\ORM\Mapping\Column` attributes.
3+
[![Latest Stable Version](https://img.shields.io/packagist/v/ensostudio/doctrine-entity-validator.svg)](https://packagist.org/packages/ensostudio/doctrine-entity-validator)
4+
[![Total Downloads](https://img.shields.io/packagist/dt/ensostudio/doctrine-entity-validator.svg)](https://packagist.org/packages/ensostudio/doctrine-entity-validator)
45

5-
Also, you can add custom validators by `EntityValidator::addValidator()`.
6+
By default, entity validation based on attached `\Doctrine\ORM\Mapping\Column` attributes and attributes inherited
7+
[ColumnValidator](src/ColumnValidators/ColumnValidator.php) interface.
8+
9+
Also, you can add custom validators by `EntityValidator::addValidator()` or create instance of [ColumnValidator](src/ColumnValidators/ColumnValidator.php) interface.
610

711
Validator skip validation:
812
- If `Column` attribute declared as not `updatable` and/or `insertable`
913
- Validation on persist/insert and property have `\Doctrine\ORM\Mapping\Id` attribute
1014

11-
Validator checks:
12-
- If property value is null (or not defined), but `Column` attribute not declared as `nullable` or don't have default value (`options: ['default' => '...']`)
15+
Validator checks by column type:
16+
- If property value is null (or not defined), but `Column` attribute not declared as `nullable` or/and don't have default value (`options: ['default' => '...']`)
1317
- If `Column` attribute have **numeric** type (integer, float, decimal and etc.):
1418
- If defined `unsigned` option, then property value must be more than zero
1519
- If type `decimal` and defined `precision`, then check size of value
@@ -19,13 +23,25 @@ Validator checks:
1923
- If `Column` attribute have **enum** type:
2024
- If defined `enumType`, then check is proprerty value is declared in enum class
2125

22-
## Example
26+
`ColumnValidator` attributes:
27+
- [MinLength](src/ColumnValidators/MinLength.php)
28+
- [Greater](src/ColumnValidators/Greater.php)
29+
- [Number](src/ColumnValidators/Number.php)
30+
- [Regexp](src/ColumnValidators/Regexp.php)
31+
- [Filter](src/ColumnValidators/Filter.php)
32+
- [Slug](src/ColumnValidators/Slug.php)
33+
- [Ip](src/ColumnValidators/Ip.php)
34+
- [Url](src/ColumnValidators/Url.php)
35+
- [Email](src/ColumnValidators/Email.php)
36+
37+
## Examples
2338

2439
Validates `Product` entity before insert/update data:
2540

2641
```php
2742
use Doctrine\DBAL\Types\Types;
2843
use Doctrine\ORM\Mapping as ORM;
44+
use \EnsoStudio\Doctrine\ORM\ColumnValidators;
2945
use EnsoStudio\Doctrine\ORM\EntityValidator;
3046
use EnsoStudio\Doctrine\ORM\EntityValidationException;
3147

@@ -36,19 +52,25 @@ class Product
3652
{
3753
...
3854

39-
#[ORM\Column(type: Types::STRING, length: 255)]
55+
#[ORM\Column(type: Types::STRING, length: 200)]
56+
#[ColumnValidators\MinLength(2)]
57+
#[ColumnValidators\Slug]
58+
private string $slug;
59+
60+
#[ORM\Column(type: Types::STRING, length: 150)]
4061
private string $name;
4162

4263
#[ORM\PrePersist]
4364
public function beforeInsert(): void
4465
{
4566
$validator = new EntityValidator($this);
67+
// Callback same to ColumnValidators\MinLength(3)
4668
$validator->addValidator(
4769
'name',
48-
static function (mixed $propertyValue, string $propertyName, object $entity) {
70+
static function (string $propertyValue, string $propertyName, object $entity) {
4971
if (mb_strlen($propertyValue) < 3) {
5072
throw new EntityValidationException(
51-
['% less than 3 chars', $propertyName],
73+
['% less than 3 characters', $propertyName],
5274
$propertyName,
5375
$entity
5476
);
@@ -66,4 +88,32 @@ class Product
6688
$validator->validate(true);
6789
}
6890
}
91+
```
92+
93+
Or you can use `EntityValidationSubscriber` to validates all entities:
94+
95+
```php
96+
use Doctrine\ORM\EntityManager;
97+
use EnsoStudio\Doctrine\ORM\EntityValidationSubscriber;
98+
99+
...
100+
$entityManager = new EntityManager($connection, $config);
101+
$entityManager->getEventManager()
102+
->addEventSubscriber(new EntityValidationSubscriber(true));
103+
```
104+
105+
## Requirements
106+
107+
- PHP >= 8.1 (with `mbstring` extension)
108+
- doctrine/orm >= 3.3
109+
110+
## Installation
111+
112+
If you do not have Composer, you may install it by following the instructions at
113+
[getcomposer.org](https://getcomposer.org/doc/00-intro.md#introduction).
114+
115+
You can then install this library using the following command:
116+
117+
```shell
118+
composer require ensostudio/doctrine-entity-validator
69119
```

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
],
1515
"require": {
1616
"php": ">=8.1",
17+
"ext-mbstring": "*",
1718
"doctrine/orm": "^3.3.2"
1819
},
1920
"require-dev": {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace EnsoStudio\Doctrine\ORM\ColumnValidators;
4+
5+
use Attribute;
6+
7+
/**
8+
* Attribute to validate property/column value of entity.
9+
*
10+
* @property-read bool $onPersist If true, validates column on persist/insert entity
11+
* @property-read bool $onUpdate If true, validates column on update entity
12+
*/
13+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
14+
interface ColumnValidator
15+
{
16+
/**
17+
* Validates property value.
18+
*
19+
* @param mixed $propertyValue The property value
20+
* @param string $propertyName The property name
21+
* @param object $entity The target entity
22+
* @throws \EnsoStudio\Doctrine\ORM\EntityValidationException If validation failed
23+
*/
24+
public function validate(mixed $propertyValue, string $propertyName, object $entity): void;
25+
}

src/ColumnValidators/Email.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace EnsoStudio\Doctrine\ORM\ColumnValidators;
4+
5+
use EnsoStudio\Doctrine\ORM\EntityValidationException;
6+
7+
/**
8+
* Validates whether the column value is a "valid" e-mail address.
9+
*
10+
* The validation is performed against the addr-spec syntax in RFC 822. However, comments, whitespace folding, and
11+
* dotless domain names are not supported, and thus will be rejected.
12+
*/
13+
class Email implements ColumnValidator
14+
{
15+
/**
16+
* @param bool $unicode Accepts Unicode characters in the local part
17+
* @param string $message The template of error message in format `sprintf()`
18+
* @param bool $onPersist If true, validates column on persist entity
19+
* @param bool $onUpdate If true, validates column on update entity
20+
*/
21+
public function __construct(
22+
public readonly bool $unicode = false,
23+
public readonly string $message = '%s: is invalid e-mail address',
24+
public readonly bool $onPersist = true,
25+
public readonly bool $onUpdate = true
26+
) {
27+
}
28+
29+
public function validate(mixed $propertyValue, string $propertyName, object $entity): void
30+
{
31+
$options = $this->unicode ? FILTER_FLAG_EMAIL_UNICODE : 0;
32+
33+
if (filter_var($propertyValue, FILTER_VALIDATE_EMAIL, $options) === false) {
34+
throw new EntityValidationException([$this->message, $propertyName], $propertyName, $entity);
35+
}
36+
}
37+
}

src/ColumnValidators/Filter.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace EnsoStudio\Doctrine\ORM\ColumnValidators;
4+
5+
use EnsoStudio\Doctrine\ORM\EntityValidationException;
6+
7+
/**
8+
* Validates column value using `filter_var()`.
9+
*/
10+
class Filter implements ColumnValidator
11+
{
12+
public const FILTERS = [
13+
'int' => FILTER_VALIDATE_INT,
14+
'float' => FILTER_VALIDATE_FLOAT,
15+
'regexp' => FILTER_VALIDATE_REGEXP,
16+
'url' => FILTER_VALIDATE_URL,
17+
'domain' => FILTER_VALIDATE_DOMAIN,
18+
'email' => FILTER_VALIDATE_EMAIL,
19+
'ip' => FILTER_VALIDATE_IP,
20+
'mac' => FILTER_VALIDATE_MAC
21+
];
22+
23+
/**
24+
* @param int $filter The ID of the validation filter to use, `FILTER_VALIDATE_...` constant
25+
* @param array<string, mixed> $options The options of validation filter
26+
* @param int $flags The bitwise disjunction of filter flags
27+
* @param string $message The template of error message in format `sprintf()`
28+
* @param bool $onPersist If true, validates column on persist entity
29+
* @param bool $onUpdate If true, validates column on update entity
30+
* @see self::FILTERS Get filter ID by short name
31+
*/
32+
public function __construct(
33+
public readonly int $filter,
34+
public readonly array $options = [],
35+
public readonly int $flags = 0,
36+
public readonly string $message = '%s: is invalid value',
37+
public readonly bool $onPersist = true,
38+
public readonly bool $onUpdate = true
39+
) {
40+
}
41+
42+
public function validate(mixed $propertyValue, string $propertyName, object $entity): void
43+
{
44+
$options = ['options' => $this->options, 'flags' => $this->flags];
45+
46+
if (filter_var($propertyValue, $this->filter, $options) === false) {
47+
throw new EntityValidationException([$this->message, $propertyName], $propertyName, $entity);
48+
}
49+
}
50+
}

src/ColumnValidators/Greater.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace EnsoStudio\Doctrine\ORM\ColumnValidators;
4+
5+
use EnsoStudio\Doctrine\ORM\EntityValidationException;
6+
7+
/**
8+
* Validates whether the column value is greater than (or equal to) range value.
9+
*/
10+
class Greater implements ColumnValidator
11+
{
12+
/**
13+
* @param float|int $minRange The minimum value
14+
* @param bool $strict If true, exclude `$minRange` value at check
15+
* @param string $message The template of error message in format `sprintf()`
16+
* @param bool $onPersist If true, validates column on persist entity
17+
* @param bool $onUpdate If true, validates column on update entity
18+
*/
19+
public function __construct(
20+
public readonly float|int $minRange,
21+
public readonly bool $strict = false,
22+
public readonly string $message = '%s: is less than %d',
23+
public readonly bool $onPersist = true,
24+
public readonly bool $onUpdate = true
25+
) {
26+
}
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public function validate(mixed $propertyValue, string $propertyName, object $entity): void
32+
{
33+
if ($this->strict ? $this->minRange >= $propertyValue : $this->minRange > $propertyValue) {
34+
throw new EntityValidationException(
35+
[$this->message, $propertyName, $this->minRange],
36+
$propertyName,
37+
$entity
38+
);
39+
}
40+
}
41+
}

src/ColumnValidators/Ip.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace EnsoStudio\Doctrine\ORM\ColumnValidators;
4+
5+
use EnsoStudio\Doctrine\ORM\EntityValidationException;
6+
7+
/**
8+
* Validates column value as IP address.
9+
*/
10+
class Ip implements ColumnValidator
11+
{
12+
/**
13+
* @param bool $ipv4 Allow IPv4 address
14+
* @param bool $ipv6 Allow IPv6 address
15+
* @param bool $noResRange Deny reserved addresses. These are the ranges that are marked as Reserved-By-Protocol in
16+
* RFC 6890. Which for IPv4 corresponds to the following ranges: `0.0.0.0/8`, `169.254.0.0/16`, `127.0.0.0/8`,
17+
* `240.0.0.0/4`. And for IPv6 corresponds to the following ranges: `::1/128`, `::/128`, `::FFFF:0:0/96`,
18+
* `FE80::/10`.
19+
* @param bool $noPrivRange Deny private addresses. These are IPv4 addresses which are in the following ranges:
20+
* `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. These are IPv6 addresses starting with FD or FC.
21+
* @param string $message The template of error message in format `sprintf()`
22+
* @param bool $onPersist If true, validates column on persist entity
23+
* @param bool $onUpdate If true, validates column on update entity
24+
*/
25+
public function __construct(
26+
public readonly bool $ipv4 = false,
27+
public readonly bool $ipv6 = false,
28+
public readonly bool $noResRange = false,
29+
public readonly bool $noPrivRange = false,
30+
public readonly string $message = '%s: is invalid IP address',
31+
public readonly bool $onPersist = true,
32+
public readonly bool $onUpdate = true
33+
) {
34+
}
35+
36+
public function validate(mixed $propertyValue, string $propertyName, object $entity): void
37+
{
38+
$options = 0;
39+
if ($this->ipv4) {
40+
$options = $options| FILTER_FLAG_IPV4;
41+
}
42+
if ($this->ipv6) {
43+
$options = $options | FILTER_FLAG_IPV6;
44+
}
45+
if ($this->noResRange) {
46+
$options = $options | FILTER_FLAG_NO_RES_RANGE;
47+
}
48+
if ($this->noPrivRange) {
49+
$options = $options | FILTER_FLAG_NO_PRIV_RANGE;
50+
}
51+
52+
if (filter_var($propertyValue, FILTER_VALIDATE_IP, $options) === false) {
53+
throw new EntityValidationException([$this->message, $propertyName], $propertyName, $entity);
54+
}
55+
}
56+
}

src/ColumnValidators/MinLength.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace EnsoStudio\Doctrine\ORM\ColumnValidators;
4+
5+
use EnsoStudio\Doctrine\ORM\EntityValidationException;
6+
7+
/**
8+
* Validates column value is long enough.
9+
*/
10+
class MinLength implements ColumnValidator
11+
{
12+
/**
13+
* @param int $length The minimum length required
14+
* @param string|null $encoding The character encoding. If it's null, the internal character encoding value will be
15+
* used.
16+
* @param string $message The template of error message in format `sprintf()`
17+
* @param bool $onPersist If true, validates column on persist entity
18+
* @param bool $onUpdate If true, validates column on update entity
19+
*/
20+
public function __construct(
21+
public readonly int $length,
22+
public readonly ?string $encoding = null,
23+
public readonly string $message = '%s: is less than %d characters',
24+
public readonly bool $onPersist = true,
25+
public readonly bool $onUpdate = true
26+
) {
27+
}
28+
29+
public function validate(mixed $propertyValue, string $propertyName, object $entity): void
30+
{
31+
if ($this->length > mb_strlen($propertyValue, $this->encoding)) {
32+
throw new EntityValidationException([$this->message, $propertyName, $this->length], $propertyName, $entity);
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)