Skip to content

Commit

Permalink
Add lazy validation feature (#734)
Browse files Browse the repository at this point in the history
* Add lazy validation feature

* test: add mock for validator in lazy validation tests

---------

Co-authored-by: Deeka Wong <[email protected]>
  • Loading branch information
huangdijia and huangdijia authored Nov 7, 2024
1 parent f952621 commit 440a354
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 18 deletions.
49 changes: 33 additions & 16 deletions src/validated-dto/src/SimpleDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,36 @@ abstract class SimpleDTO implements BaseDTO, CastsAttributes
use DataResolver;
use DataTransformer;

public bool $lazyValidation = false;

/** @internal */
protected array $dtoData = [];

/** @internal */
protected array $validatedData = [];

/** @internal */
protected bool $requireCasting = false;

/** @internal */
protected ?ValidatorInterface $validator = null;

/** @internal */
protected array $dtoRules = [];

/** @internal */
protected array $dtoMessages = [];

/** @internal */
protected array $dtoDefaults = [];

/** @internal */
protected array $dtoCasts = [];

/** @internal */
protected array $dtoMapData = [];

/** @internal */
protected array $dtoMapTransform = [];

/**
Expand Down Expand Up @@ -161,9 +173,9 @@ protected function mapToTransform(): array
*
* @throws MissingCastTypeException|CastTargetException
*/
protected function passedValidation(): void
protected function passedValidation(bool $forceCast = false): void
{
$this->validatedData = $this->validatedData();
$this->validatedData = $this->validatedData($forceCast);
/** @var array<string, Castable> $casts */
$casts = $this->buildCasts();

Expand Down Expand Up @@ -194,7 +206,7 @@ protected function passedValidation(): void

$formatted = $this->shouldReturnNull($key, $value)
? null
: $this->castValue($casts[$key], $key, $value);
: $this->castValue($casts[$key], $key, $value, $forceCast);
$this->{$key} = $formatted;
$this->validatedData[$key] = $formatted;
}
Expand Down Expand Up @@ -224,7 +236,7 @@ protected function isValidData(): bool
*
* @throws MissingCastTypeException|CastTargetException
*/
protected function validatedData(): array
protected function validatedData(bool $forceCast = false): array
{
$acceptedKeys = $this->getAcceptedProperties();
$result = [];
Expand All @@ -245,7 +257,7 @@ protected function validatedData(): array

$result[$key] = $this->shouldReturnNull($key, $value)
? null
: $this->castValue($casts[$key], $key, $value);
: $this->castValue($casts[$key], $key, $value, $forceCast);
}
}

Expand All @@ -261,8 +273,12 @@ protected function validatedData(): array
/**
* @throws CastTargetException
*/
protected function castValue(mixed $cast, string $key, mixed $value): mixed
protected function castValue(mixed $cast, string $key, mixed $value, bool $forceCast = false): mixed
{
if ($this->lazyValidation && ! $forceCast) {
return $value;
}

if ($cast instanceof Castable) {
return $cast->cast($key, $value);
}
Expand Down Expand Up @@ -312,6 +328,16 @@ protected function buildDataForExport(): array
return $this->mapDTOData($mapping, $this->validatedData);
}

protected function buildDataForValidation(array $data): array
{
$mapping = [
...$this->mapData(),
...$this->dtoMapData,
];

return $this->mapDTOData($mapping, $data);
}

private function buildAttributesData(): void
{
$publicProperties = $this->getPublicProperties();
Expand Down Expand Up @@ -386,16 +412,6 @@ private function getPropertiesForAttribute(array $properties, string $attribute)
return $result;
}

private function buildDataForValidation(array $data): array
{
$mapping = [
...$this->mapData(),
...$this->dtoMapData,
];

return $this->mapDTOData($mapping, $data);
}

private function mapDTOData(array $mapping, array $data): array
{
$mappedData = [];
Expand Down Expand Up @@ -542,6 +558,7 @@ private function isForbiddenProperty(string $property): bool
'dtoCasts',
'dtoMapData',
'dtoMapTransform',
'lazyValidation',
]);
}
}
32 changes: 30 additions & 2 deletions src/validated-dto/src/ValidatedDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ public function attributes(): array
return [];
}

/**
* @throws ValidationException|MissingCastTypeException|CastTargetException
*/
public function validate(): void
{
$this->dtoData = $this->buildDataForValidation($this->toArray());

$this->validationPasses()
? $this->passedValidation(true)
: $this->failedValidation();
}

protected function after(ValidatorInterface $validator): void
{
// Do nothing
Expand Down Expand Up @@ -90,7 +102,7 @@ abstract protected function rules(): array;
*
* @throws MissingCastTypeException|CastTargetException
*/
protected function validatedData(): array
protected function validatedData(bool $forceCast = false): array
{
$acceptedKeys = array_keys($this->rulesList());
$result = [];
Expand All @@ -111,7 +123,7 @@ protected function validatedData(): array

$result[$key] = $this->shouldReturnNull($key, $value)
? null
: $this->castValue($casts[$key], $key, $value);
: $this->castValue($casts[$key], $key, $value, $forceCast);
}
}

Expand Down Expand Up @@ -174,6 +186,22 @@ private function messagesList(): array
];
}

private function validationPasses(): bool
{
$container = ApplicationContext::getContainer();
$this->validator = $container->get(ValidatorFactoryInterface::class)
->make(
$this->dtoData,
$this->rulesList(),
$this->messagesList(),
$this->attributes()
);

$this->validator->after(fn (ValidatorInterface $validator) => $this->after($validator));

return ! $this->validator->fails();
}

private function isOptionalProperty(string $property): bool
{
$rules = $this->rulesList();
Expand Down
46 changes: 46 additions & 0 deletions tests/ValidatedDTO/Datasets/LazyDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);
/**
* This file is part of friendsofhyperf/components.
*
* @link https://github.com/friendsofhyperf/components
* @document https://github.com/friendsofhyperf/components/blob/main/README.md
* @contact [email protected]
*/

namespace FriendsOfHyperf\Tests\ValidatedDTO\Datasets;

use FriendsOfHyperf\ValidatedDTO\Casting\IntegerCast;
use FriendsOfHyperf\ValidatedDTO\Casting\StringCast;
use FriendsOfHyperf\ValidatedDTO\ValidatedDTO;

class LazyDTO extends ValidatedDTO
{
public bool $lazyValidation = true;

public ?string $name;

public ?int $age = null;

protected function rules(): array
{
return [
'name' => 'required',
'age' => 'numeric',
];
}

protected function defaults(): array
{
return [];
}

protected function casts(): array
{
return [
'name' => new StringCast(),
'age' => new IntegerCast(),
];
}
}
61 changes: 61 additions & 0 deletions tests/ValidatedDTO/Unit/LazyValidationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);
/**
* This file is part of friendsofhyperf/components.
*
* @link https://github.com/friendsofhyperf/components
* @document https://github.com/friendsofhyperf/components/blob/main/README.md
* @contact [email protected]
*/
use FriendsOfHyperf\Tests\ValidatedDTO\Datasets\LazyDTO;
use FriendsOfHyperf\ValidatedDTO\ValidatedDTO;
use Hyperf\Contract\ValidatorInterface;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Validation\ValidationException;

beforeEach(function () {
$this->subject_name = faker()->name();
});

it('instantiates a ValidatedDTO marked as lazy without validating its data', function () {
$validatedDTO = new LazyDTO(['name' => $this->subject_name]);

expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
->and($validatedDTO->validatedData)
->toBe(['name' => $this->subject_name])
->and($validatedDTO->lazyValidation)
->toBeTrue();
});

it('does not fails a lazy validation with valid data', function () {
$validatedDTO = new LazyDTO(['name' => $this->subject_name]);

expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
->and($validatedDTO->validatedData)
->toBe(['name' => $this->subject_name])
->and($validatedDTO->lazyValidation)
->toBeTrue();

$validatedDTO->validate();
});

it('fails a lazy validation with invalid data', function () {
$this->mock(ValidatorFactoryInterface::class, function ($mock) {
$mock->shouldReceive('make')->andReturn(Mockery::mock(ValidatorInterface::class, function ($mock) {
$mock->shouldReceive('fails')->andReturn(true)
->shouldReceive('passes')->andReturn(false)
->shouldReceive('after')->andReturn(null);
}));
});

$validatedDTO = new LazyDTO(['name' => null]);

expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
->and($validatedDTO->validatedData)
->toBe(['name' => null])
->and($validatedDTO->lazyValidation)
->toBeTrue();

$validatedDTO->validate();
})->throws(ValidationException::class);

0 comments on commit 440a354

Please sign in to comment.