Skip to content

Commit 5d4715b

Browse files
Merge pull request #117 from WendellAdriel/feat/new-attributes
Add Lazy, Provide and Receive attributes
2 parents 650086c + 5762708 commit 5d4715b

File tree

9 files changed

+294
-1
lines changed

9 files changed

+294
-1
lines changed

src/Attributes/Lazy.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS)]
10+
final class Lazy {}

src/Attributes/Provide.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Attributes;
6+
7+
use Attribute;
8+
use WendellAdriel\ValidatedDTO\Enums\PropertyCase;
9+
10+
#[Attribute(Attribute::TARGET_CLASS)]
11+
final class Provide
12+
{
13+
public function __construct(public PropertyCase $propertyCase) {}
14+
}

src/Attributes/Receive.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Attributes;
6+
7+
use Attribute;
8+
use WendellAdriel\ValidatedDTO\Enums\PropertyCase;
9+
10+
#[Attribute(Attribute::TARGET_CLASS)]
11+
final class Receive
12+
{
13+
public function __construct(public PropertyCase $propertyCase) {}
14+
}

src/Enums/PropertyCase.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Enums;
6+
7+
use Illuminate\Support\Str;
8+
9+
enum PropertyCase
10+
{
11+
case SnakeCase;
12+
13+
case PascalCase;
14+
15+
public function format(string $value): string
16+
{
17+
return match ($this) {
18+
self::SnakeCase => Str::snake($value),
19+
self::PascalCase => Str::pascal($value),
20+
};
21+
}
22+
}

src/SimpleDTO.php

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
use Illuminate\Support\Collection;
1515
use Illuminate\Validation\ValidationException;
1616
use JsonSerializable;
17+
use ReflectionAttribute;
1718
use ReflectionClass;
1819
use ReflectionProperty;
1920
use UnitEnum;
2021
use WendellAdriel\ValidatedDTO\Attributes\Cast;
2122
use WendellAdriel\ValidatedDTO\Attributes\DefaultValue;
23+
use WendellAdriel\ValidatedDTO\Attributes\Lazy;
2224
use WendellAdriel\ValidatedDTO\Attributes\Map;
25+
use WendellAdriel\ValidatedDTO\Attributes\Provide;
26+
use WendellAdriel\ValidatedDTO\Attributes\Receive;
2327
use WendellAdriel\ValidatedDTO\Attributes\Rules;
2428
use WendellAdriel\ValidatedDTO\Casting\ArrayCast;
2529
use WendellAdriel\ValidatedDTO\Casting\Castable;
@@ -67,6 +71,9 @@ abstract class SimpleDTO implements BaseDTO, CastsAttributes, JsonSerializable
6771
/** @internal */
6872
protected array $dtoMapTransform = [];
6973

74+
/** @internal */
75+
private static array $classReflections = [];
76+
7077
/**
7178
* @throws ValidationException|MissingCastTypeException|CastTargetException
7279
*/
@@ -381,6 +388,49 @@ private function buildAttributesData(): void
381388
$this->dtoCasts[$property] = $attributeInstance;
382389
}
383390

391+
$classReflection = $this->classReflection($this::class);
392+
$classAttributes = collect($classReflection->getAttributes());
393+
$lazyAttribute = $classAttributes->first(
394+
fn (ReflectionAttribute $attribute) => $attribute->getName() === Lazy::class
395+
);
396+
/** @var ReflectionAttribute $receiveAttribute */
397+
$receiveAttribute = $classAttributes->first(
398+
fn (ReflectionAttribute $attribute) => $attribute->getName() === Receive::class
399+
);
400+
/** @var ReflectionAttribute $provideAttribute */
401+
$provideAttribute = $classAttributes->first(
402+
fn (ReflectionAttribute $attribute) => $attribute->getName() === Provide::class
403+
);
404+
405+
if (! is_null($lazyAttribute)) {
406+
$this->lazyValidation = true;
407+
}
408+
409+
$receiveCase = null;
410+
$provideCase = null;
411+
if (! is_null($receiveAttribute)) {
412+
/** @var Receive $receive */
413+
$receive = $receiveAttribute->newInstance();
414+
$receiveCase = $receive->propertyCase;
415+
}
416+
417+
if (! is_null($provideAttribute)) {
418+
/** @var Provide $provide */
419+
$provide = $provideAttribute->newInstance();
420+
$provideCase = $provide->propertyCase;
421+
}
422+
423+
if (! is_null($receiveCase) || ! is_null($provideCase)) {
424+
foreach (array_keys($publicProperties) as $property) {
425+
if (! is_null($receiveCase)) {
426+
$this->dtoMapData[$receiveCase->format($property)] = $property;
427+
}
428+
if (! is_null($provideCase)) {
429+
$this->dtoMapTransform[$property] = $provideCase->format($property);
430+
}
431+
}
432+
}
433+
384434
$mapDataProperties = $this->getPropertiesForAttribute($publicProperties, Map::class);
385435
foreach ($mapDataProperties as $property => $attribute) {
386436
$attributeInstance = $attribute->newInstance();
@@ -397,7 +447,7 @@ private function buildAttributesData(): void
397447

398448
private function getPublicProperties(): array
399449
{
400-
$reflectionClass = new ReflectionClass($this);
450+
$reflectionClass = $this->classReflection($this::class);
401451
$dtoProperties = [];
402452

403453
foreach ($reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
@@ -427,6 +477,15 @@ private function getPropertiesForAttribute(array $properties, string $attribute)
427477
return $result;
428478
}
429479

480+
private function classReflection(string $class): ReflectionClass
481+
{
482+
if (! isset(self::$classReflections[$class])) {
483+
self::$classReflections[$class] = new ReflectionClass($class);
484+
}
485+
486+
return self::$classReflections[$class];
487+
}
488+
430489
private function mapDTOData(array $mapping, array $data): array
431490
{
432491
$mappedData = [];
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Tests\Datasets;
6+
7+
use WendellAdriel\ValidatedDTO\Attributes\Lazy;
8+
use WendellAdriel\ValidatedDTO\Casting\IntegerCast;
9+
use WendellAdriel\ValidatedDTO\Casting\StringCast;
10+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
11+
use WendellAdriel\ValidatedDTO\ValidatedDTO;
12+
13+
#[Lazy]
14+
class LazyAttributeDTO extends ValidatedDTO
15+
{
16+
use EmptyDefaults;
17+
18+
public ?string $name;
19+
20+
public ?int $age = null;
21+
22+
protected function rules(): array
23+
{
24+
return [
25+
'name' => 'required',
26+
'age' => 'numeric',
27+
];
28+
}
29+
30+
protected function casts(): array
31+
{
32+
return [
33+
'name' => new StringCast(),
34+
'age' => new IntegerCast(),
35+
];
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Tests\Datasets;
6+
7+
use WendellAdriel\ValidatedDTO\Attributes\Provide;
8+
use WendellAdriel\ValidatedDTO\Attributes\Receive;
9+
use WendellAdriel\ValidatedDTO\Attributes\Rules;
10+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
11+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
12+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
13+
use WendellAdriel\ValidatedDTO\Enums\PropertyCase;
14+
use WendellAdriel\ValidatedDTO\ValidatedDTO;
15+
16+
#[Receive(PropertyCase::SnakeCase)]
17+
#[Provide(PropertyCase::PascalCase)]
18+
final class ProvideReceiveDTO extends ValidatedDTO
19+
{
20+
use EmptyCasts,
21+
EmptyDefaults,
22+
EmptyRules;
23+
24+
#[Rules(['required', 'string'])]
25+
public string $firstName;
26+
27+
#[Rules(['required', 'string'])]
28+
public string $lastName;
29+
30+
#[Rules(['required', 'integer'])]
31+
public int $age;
32+
}

tests/Unit/LazyValidationTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use Illuminate\Validation\ValidationException;
6+
use WendellAdriel\ValidatedDTO\Tests\Datasets\LazyAttributeDTO;
67
use WendellAdriel\ValidatedDTO\Tests\Datasets\LazyDTO;
78
use WendellAdriel\ValidatedDTO\ValidatedDTO;
89

@@ -43,3 +44,43 @@
4344

4445
$validatedDTO->validate();
4546
})->throws(ValidationException::class);
47+
48+
it('instantiates a ValidatedDTO with Lazy attribute without validating its data', function () {
49+
$name = fake()->name;
50+
$validatedDTO = new LazyAttributeDTO(['name' => $name]);
51+
52+
expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
53+
->and($validatedDTO->validatedData)
54+
->toBe(['name' => $name])
55+
->and($validatedDTO->lazyValidation)
56+
->toBeTrue();
57+
});
58+
59+
it('does not fail lazy validation with Lazy attribute when valid data is provided', function () {
60+
$name = fake()->name;
61+
$age = fake()->numberBetween(18, 80);
62+
$validatedDTO = new LazyAttributeDTO([
63+
'name' => $name,
64+
'age' => $age,
65+
]);
66+
67+
expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
68+
->and($validatedDTO->validatedData)
69+
->toBe(['name' => $name, 'age' => $age])
70+
->and($validatedDTO->lazyValidation)
71+
->toBeTrue();
72+
73+
$validatedDTO->validate();
74+
});
75+
76+
it('fails lazy validation with Lazy attribute when invalid data is provided', function () {
77+
$validatedDTO = new LazyAttributeDTO(['name' => null]);
78+
79+
expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
80+
->and($validatedDTO->validatedData)
81+
->toBe(['name' => null])
82+
->and($validatedDTO->lazyValidation)
83+
->toBeTrue();
84+
85+
$validatedDTO->validate();
86+
})->throws(ValidationException::class);

tests/Unit/ProvideReceiveTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use WendellAdriel\ValidatedDTO\Tests\Datasets\ProvideReceiveDTO;
6+
use WendellAdriel\ValidatedDTO\ValidatedDTO;
7+
8+
it('instantiates a ValidatedDTO receiving data in snake_case', function () {
9+
$firstName = fake()->firstName;
10+
$lastName = fake()->lastName;
11+
$age = fake()->numberBetween(18, 80);
12+
13+
$validatedDTO = new ProvideReceiveDTO([
14+
'first_name' => $firstName,
15+
'last_name' => $lastName,
16+
'age' => $age,
17+
]);
18+
19+
expect($validatedDTO)->toBeInstanceOf(ValidatedDTO::class)
20+
->and($validatedDTO->firstName)->toBe($firstName)
21+
->and($validatedDTO->lastName)->toBe($lastName)
22+
->and($validatedDTO->age)->toBe($age);
23+
});
24+
25+
it('provides data in PascalCase when calling toArray', function () {
26+
$firstName = fake()->firstName;
27+
$lastName = fake()->lastName;
28+
$age = fake()->numberBetween(18, 80);
29+
30+
$validatedDTO = new ProvideReceiveDTO([
31+
'first_name' => $firstName,
32+
'last_name' => $lastName,
33+
'age' => $age,
34+
]);
35+
36+
$array = $validatedDTO->toArray();
37+
38+
expect($array)->toBe([
39+
'FirstName' => $firstName,
40+
'LastName' => $lastName,
41+
'Age' => $age,
42+
]);
43+
});
44+
45+
it('provides data in PascalCase when calling toJson', function () {
46+
$firstName = fake()->firstName;
47+
$lastName = fake()->lastName;
48+
$age = fake()->numberBetween(18, 80);
49+
50+
$validatedDTO = new ProvideReceiveDTO([
51+
'first_name' => $firstName,
52+
'last_name' => $lastName,
53+
'age' => $age,
54+
]);
55+
56+
$json = $validatedDTO->toJson();
57+
$decoded = json_decode($json, true);
58+
59+
expect($decoded)->toBe([
60+
'FirstName' => $firstName,
61+
'LastName' => $lastName,
62+
'Age' => $age,
63+
]);
64+
});

0 commit comments

Comments
 (0)