Skip to content

Commit f02b3c5

Browse files
committed
Enhance default value handling with DefaultsTo improvements
1 parent 485086d commit f02b3c5

File tree

8 files changed

+174
-13
lines changed

8 files changed

+174
-13
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# PHP Pure Data objects
22

3-
![Packagist Version](https://img.shields.io/packagist/v/nuxtifyts/php-dto)
3+
![Packagist Version](https://img.shields.io/packagist/v/nuxtifyts/php-dto?style=for-the-badge&cacheSeconds=3600)
4+
![PhpStan Level](https://img.shields.io/badge/PHPStan-level%2010-brightgreen.svg?style=for-the-badge)
45

56
This package enabled the creation of data objects which can be used in various ways.
67
Using modern PHP syntax, it allows you to hydrate data from arrays, objects, and other data sources.

docs/PropertyAttributes.md

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Property Attributes
44
In order to provide more functionality to your DTOs, you can use the following attributes:
55
- [Computed](#Computed) - To define a property that is computed from other properties.
66
- [Aliases](#Aliases) - To define aliases for a property.
7-
- [DefaultsUsing](#DefaultsUsing) - To define a default value for a property using a fallback resolver.
7+
- [DefaultsTo](#DefaultsTo) - To define a default value for a property using a fallback resolver.
88
- [CipherTarget](#CipherTarget) - To define a property that should be encrypted/decrypted.
99

1010
Computed
@@ -110,7 +110,7 @@ public function __construct(
110110
) {}
111111
```
112112

113-
DefaultsUsing
113+
DefaultsTo
114114
-
115115

116116
Sometimes, we may need to specify that a property has a default value,
@@ -134,24 +134,50 @@ final readonly class User extends Data
134134
On the other hand, if we want to specify, for example, a default value for UserType depending
135135
on the provided email address, or if you want to provide a default value for complex data such as
136136
`UserConfigData` which is another DTO, there is no way to do it using plain PHP,
137-
that's where `DefaultsUsing` attribute comes in.
137+
that's where `DefaultsTo` attribute comes in.
138138

139139
```php
140140
use Nuxtifyts\PhpDto\Data;
141-
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsUsing;
141+
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
142142

143143
final readonly class User extends Data
144144
{
145145
public function __construct(
146146
public string $firstName,
147147
public string $lastName,
148148
public string $email,
149-
#[DefaultsUsing(UserTypeFallbackResolver::class)]
149+
#[DefaultsTo(UserType::DEFAULT)]
150150
public UserType $type,
151-
#[DefaultsUsing(UserConfigDataFallbackResolver::class)]
151+
#[DefaultsTo(UserConfigDataFallbackResolver::class)]
152152
public UserConfigData $config,
153153
) {}
154154
}
155155
```
156156

157-
TODO - Add example of fall back resolver code
157+
The `DefaultsTo` attribute provides the ability to specify default values for complex types,
158+
such as DateTimes and DTOs.
159+
160+
In this example, the `UserConfigDataFallbackResolver` would look like this:
161+
162+
```php
163+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
164+
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;
165+
166+
class UserConfigDataFallbackResolver implements FallbackResolver
167+
{
168+
/**
169+
* @param array<string, mixed> $rawData
170+
*/
171+
public static function resolve(array $rawData, PropertyContext $property) : mixed{
172+
$email = $rawData['email'] ?? null;
173+
174+
return match(true) {
175+
str_contains($email, 'example.com') => new UserConfigData(/** Admin configs */),
176+
default => new UserConfigData(/** User configs */)
177+
}
178+
}
179+
}
180+
```
181+
182+
>! When using `DefaultsTo` attribute, priority is given to the attribute instead of the parameter's default value.
183+

src/Attributes/Property/DefaultsTo.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ class DefaultsTo
1818
protected(set) ?string $fallbackResolverClass = null;
1919

2020
/**
21+
* @param BackedEnum|array<array-key, mixed>|int|string|float|bool|null $value
22+
*
2123
* @throws FallbackResolverException
2224
*/
2325
public function __construct(
24-
protected(set) BackedEnum|int|string|float|bool|null $value
26+
protected(set) BackedEnum|array|int|string|float|bool|null $value
2527
) {
2628
if (is_string($value) && class_exists($value)) {
2729
/** @var ReflectionClass<object> $reflection */

src/FallbackResolver/FallbackConfig.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
readonly class FallbackConfig
88
{
99
/**
10+
* @param BackedEnum|array<array-key, mixed>|int|string|float|bool|null $value
1011
* @param ?class-string<FallbackResolver> $resolverClass
1112
*/
1213
public function __construct(
13-
public BackedEnum|int|string|float|bool|null $value,
14+
public BackedEnum|array|int|string|float|bool|null $value,
1415
public ?string $resolverClass = null
1516
) {
1617
}

src/Pipelines/DeserializePipeline/ResolveDefaultDataPipe.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Nuxtifyts\PhpDto\Support\Passable;
66
use Nuxtifyts\PhpDto\Support\Pipe;
7+
use ReflectionParameter;
78

89
/**
910
* @extends Pipe<DeserializePipelinePassable>
@@ -25,8 +26,21 @@ public function handle(Passable $passable): DeserializePipelinePassable
2526
: $propertyContext->fallbackConfig->value;
2627
}
2728

28-
if ($propertyContext->reflection->hasDefaultValue()) {
29-
$data[$propertyContext->propertyName] = $propertyContext->reflection->getDefaultValue();
29+
$constructorParameters = $propertyContext->reflection
30+
->getDeclaringClass()
31+
->getConstructor()
32+
?->getParameters();
33+
34+
if (
35+
$propertyParameter = array_find(
36+
$constructorParameters ?? [],
37+
fn (ReflectionParameter $parameter) => $parameter->getName() === $propertyContext->propertyName
38+
)
39+
) {
40+
/** @var ReflectionParameter $propertyParameter */
41+
if ($propertyParameter->isDefaultValueAvailable()) {
42+
$data[$propertyContext->propertyName] = $propertyParameter->getDefaultValue();
43+
}
3044
}
3145
}
3246

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Dummies\FallbackResolvers;
4+
5+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
6+
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;
7+
use Nuxtifyts\PhpDto\Tests\Dummies\UserData;
8+
9+
class DummyUserFallbackResolver implements FallbackResolver
10+
{
11+
public static function resolve(array $rawData, PropertyContext $property): mixed
12+
{
13+
if (array_key_exists($property->propertyName, $rawData)) {
14+
return $rawData[$property->propertyName];
15+
}
16+
17+
return new UserData(
18+
'John',
19+
'Doe'
20+
);
21+
}
22+
}

tests/Unit/Attributes/DefaultsToTest.php

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,29 @@
33
namespace Nuxtifyts\PhpDto\Tests\Unit\Attributes;
44

55
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
6+
use Nuxtifyts\PhpDto\Attributes\Property\Types\ArrayOfScalarTypes;
67
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
78
use Nuxtifyts\PhpDto\Data;
9+
use Nuxtifyts\PhpDto\Enums\Property\Type;
810
use Nuxtifyts\PhpDto\Exceptions\FallbackResolverException;
911
use Nuxtifyts\PhpDto\FallbackResolver\FallbackConfig;
12+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveDefaultDataPipe;
13+
use Nuxtifyts\PhpDto\Tests\Dummies\FallbackResolvers\DummyUserFallbackResolver;
1014
use Nuxtifyts\PhpDto\Tests\Dummies\UserData;
1115
use Nuxtifyts\PhpDto\Tests\Unit\UnitCase;
1216
use PHPUnit\Framework\Attributes\CoversClass;
1317
use PHPUnit\Framework\Attributes\DataProvider;
1418
use PHPUnit\Framework\Attributes\Test;
19+
use PHPUnit\Framework\Attributes\UsesClass;
1520
use Throwable;
1621

1722
#[CoversClass(DefaultsTo::class)]
1823
#[CoversClass(FallbackResolverException::class)]
1924
#[CoversClass(FallbackConfig::class)]
2025
#[CoversClass(PropertyContext::class)]
26+
#[CoversClass(ResolveDefaultDataPipe::class)]
27+
#[UsesClass(DummyUserFallbackResolver::class)]
28+
#[UsesClass(UserData::class)]
2129
final class DefaultsToTest extends UnitCase
2230
{
2331
/**
@@ -68,7 +76,70 @@ public function __construct(
6876
'expectedSerializedData' => [
6977
'name' => 'John'
7078
]
71-
]
79+
],
80+
'Resolves default complex type value' => [
81+
'object' => new readonly class (
82+
new UserData('John', 'Doe')
83+
) extends Data {
84+
public function __construct(
85+
#[DefaultsTo(DummyUserFallbackResolver::class)]
86+
public UserData $userData
87+
) {
88+
}
89+
},
90+
'arrayData' => [],
91+
'expectedSerializedData' => [
92+
'userData' => [
93+
'firstName' => 'John',
94+
'lastName' => 'Doe'
95+
]
96+
]
97+
],
98+
'Resolves array of scalar type values' => [
99+
'object' => new readonly class([]) extends Data {
100+
/**
101+
* @param list<string> $names
102+
*/
103+
public function __construct(
104+
#[ArrayOfScalarTypes(Type::STRING)]
105+
#[DefaultsTo(['John', 'Jane'])]
106+
public array $names
107+
) {
108+
}
109+
},
110+
'arrayData' => [],
111+
'expectedSerializedData' => [
112+
'names' => ['John', 'Jane']
113+
]
114+
],
115+
'Allows pure php way of defaulting 1' => [
116+
'object' => new readonly class ('') extends Data {
117+
public function __construct(
118+
public string $name = 'John'
119+
) {
120+
}
121+
},
122+
'arrayData' => [],
123+
'expectedSerializedData' => [
124+
'name' => 'John'
125+
]
126+
],
127+
'Allows pure php way of defaulting 2' => [
128+
'object' => new readonly class([]) extends Data {
129+
/**
130+
* @param list<string> $names
131+
*/
132+
public function __construct(
133+
#[ArrayOfScalarTypes(Type::STRING)]
134+
public array $names = ['John', 'Jane']
135+
) {
136+
}
137+
},
138+
'arrayData' => [],
139+
'expectedSerializedData' => [
140+
'names' => ['John', 'Jane']
141+
]
142+
],
72143
];
73144
}
74145
}

tests/Unit/DataRefiners/DateTimeRefinerTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
77
use Nuxtifyts\PhpDto\Data;
88
use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner;
9+
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
910
use Nuxtifyts\PhpDto\Exceptions\InvalidRefiner;
1011
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe;
1112
use Nuxtifyts\PhpDto\Serializers\DateTimeSerializer;
@@ -160,4 +161,27 @@ public function __construct(
160161
self::assertInstanceOf(DateTimeImmutable::class, $object2->date);
161162
self::assertEquals($now->format('Y-m-d'), $object2->date->format('Y-m-d'));
162163
}
164+
165+
/**
166+
* @throws Throwable
167+
*/
168+
#[Test]
169+
public function will_fail_to_refine_value_if_wrong_value_is_used(): void
170+
{
171+
$object = new readonly class (null) extends Data {
172+
public function __construct(
173+
#[WithRefiner(DateTimeRefiner::class)]
174+
public ?DateTimeImmutable $date
175+
) {
176+
}
177+
};
178+
179+
$now = new DateTimeImmutable();
180+
181+
self::expectException(DeserializeException::class);
182+
183+
$object::from([
184+
'date' => $now->format('Y/m-d')
185+
]);
186+
}
163187
}

0 commit comments

Comments
 (0)