Skip to content

Commit 3b2dbc4

Browse files
authored
Merge pull request #5 from nuxtifyts/feature/fallback-resolvers
Added default to attribute to specify default values for properties
2 parents 37fd091 + e3a305a commit 3b2dbc4

27 files changed

+725
-26
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/DefaultValues.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
Default Values
2+
=
3+
4+
Sometimes, we may need to specify that a property has a default value,
5+
we can achieve that using plain PHP for some property types but not all of them.
6+
7+
```php
8+
use Nuxtifyts\PhpDto\Data;
9+
10+
final readonly class User extends Data
11+
{
12+
public function __construct(
13+
public string $firstName,
14+
public string $lastName,
15+
public string $email,
16+
public UserType $type = UserType::DEFAULT,
17+
public UserConfigData $config,
18+
) {}
19+
}
20+
```
21+
22+
On the other hand, if we want to specify, for example, a default value for UserType depending
23+
on the provided email address, or if you want to provide a default value for complex data such as
24+
`UserConfigData` which is another DTO, there is no way to do it using plain PHP,
25+
that's where `DefaultsTo` attribute comes in.
26+
27+
```php
28+
use Nuxtifyts\PhpDto\Data;
29+
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
30+
31+
final readonly class User extends Data
32+
{
33+
public function __construct(
34+
public string $firstName,
35+
public string $lastName,
36+
public string $email,
37+
#[DefaultsTo(UserType::DEFAULT)]
38+
public UserType $type,
39+
#[DefaultsTo(UserConfigDataFallbackResolver::class)]
40+
public UserConfigData $config,
41+
) {}
42+
}
43+
```
44+
45+
The `DefaultsTo` attribute provides the ability to specify default values for complex types,
46+
such as DateTimes and DTOs.
47+
48+
For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md)
49+
guide.
50+
51+
In this example, the `UserConfigDataFallbackResolver` would look like this:
52+
53+
```php
54+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
55+
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;
56+
57+
class UserConfigDataFallbackResolver implements FallbackResolver
58+
{
59+
/**
60+
* @param array<string, mixed> $rawData
61+
*/
62+
public static function resolve(array $rawData, PropertyContext $property) : mixed{
63+
$email = $rawData['email'] ?? null;
64+
65+
return match(true) {
66+
str_contains($email, 'example.com') => new UserConfigData(/** Admin configs */),
67+
default => new UserConfigData(/** User configs */)
68+
}
69+
}
70+
}
71+
```
72+
73+
>! When using `DefaultsTo` attribute, priority is given to the attribute instead of the parameter's default value.
74+
75+
If ever needed to create a new instance of a DTO with complex default value,
76+
using the constructor is no longer possible, instead, you can make use of the
77+
`create` function provided by the DTO class.
78+
79+
Using the same example above, we can create a new instance of `User` with the default value for `config`:
80+
81+
```php
82+
$user = User::create(
83+
firstName: 'John',
84+
lastName: 'Doe',
85+
86+
);
87+
```

docs/PropertyAttributes.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +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+
- [DefaultsTo](#DefaultsTo) - To define a default value for a property using a fallback resolver.
78
- [CipherTarget](#CipherTarget) - To define a property that should be encrypted/decrypted.
89

910
Computed
@@ -109,3 +110,52 @@ public function __construct(
109110
) {}
110111
```
111112

113+
DefaultsTo
114+
-
115+
116+
Sometimes, we may need to specify that a property has a default value,
117+
we can achieve that using plain PHP for some property types but not all of them.
118+
119+
```php
120+
use Nuxtifyts\PhpDto\Data;
121+
122+
final readonly class User extends Data
123+
{
124+
public function __construct(
125+
public string $firstName,
126+
public string $lastName,
127+
public string $email,
128+
public UserType $type = UserType::DEFAULT,
129+
public UserConfigData $config,
130+
) {}
131+
}
132+
```
133+
134+
On the other hand, if we want to specify, for example, a default value for UserType depending
135+
on the provided email address, or if you want to provide a default value for complex data such as
136+
`UserConfigData` which is another DTO, there is no way to do it using plain PHP,
137+
that's where `DefaultsTo` attribute comes in.
138+
139+
```php
140+
use Nuxtifyts\PhpDto\Data;
141+
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
142+
143+
final readonly class User extends Data
144+
{
145+
public function __construct(
146+
public string $firstName,
147+
public string $lastName,
148+
public string $email,
149+
#[DefaultsTo(UserType::DEFAULT)]
150+
public UserType $type,
151+
#[DefaultsTo(UserConfigDataFallbackResolver::class)]
152+
public UserConfigData $config,
153+
) {}
154+
}
155+
```
156+
157+
The `DefaultsTo` attribute provides the ability to specify default values for complex types,
158+
such as DateTimes and DTOs.
159+
160+
For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md)
161+
guide.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Attributes\Property;
4+
5+
use Attribute;
6+
use BackedEnum;
7+
use Nuxtifyts\PhpDto\Exceptions\FallbackResolverException;
8+
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;
9+
use ReflectionClass;
10+
11+
#[Attribute(Attribute::TARGET_PROPERTY)]
12+
class DefaultsTo
13+
{
14+
/** @var array<string, ReflectionClass<object>> */
15+
protected static array $_resolverReflections = [];
16+
17+
/** @var ?class-string<FallbackResolver> */
18+
protected(set) ?string $fallbackResolverClass = null;
19+
20+
/**
21+
* @param BackedEnum|array<array-key, mixed>|int|string|float|bool|null $value
22+
*
23+
* @throws FallbackResolverException
24+
*/
25+
public function __construct(
26+
protected(set) BackedEnum|array|int|string|float|bool|null $value
27+
) {
28+
if (is_string($value) && class_exists($value)) {
29+
/** @var ReflectionClass<object> $reflection */
30+
$reflection = self::$_resolverReflections[$value] ??= new ReflectionClass($value);
31+
32+
if (!$reflection->implementsInterface(FallbackResolver::class)) {
33+
throw FallbackResolverException::unableToFindResolverClass($value);
34+
} else {
35+
/** @var class-string<FallbackResolver> $value */
36+
$this->fallbackResolverClass = $value;
37+
}
38+
}
39+
}
40+
}

src/Concerns/BaseData.php

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
namespace Nuxtifyts\PhpDto\Concerns;
44

55
use Nuxtifyts\PhpDto\Contexts\ClassContext;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
67
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
78
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
8-
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DecipherDataPipe;
9+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline;
910
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
10-
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe;
11-
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe;
12-
use Nuxtifyts\PhpDto\Support\Pipeline;
1311
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
1412
use ReflectionClass;
1513
use Throwable;
@@ -18,6 +16,40 @@ trait BaseData
1816
{
1917
use HasNormalizers;
2018

19+
final public static function create(mixed ...$args): static
20+
{
21+
if (array_any(
22+
array_keys($args),
23+
static fn (string|int $arg) => is_numeric($arg)
24+
)) {
25+
throw DataCreationException::invalidProperty();
26+
}
27+
28+
try {
29+
$value = static::normalizeValue($args, static::class);
30+
31+
if ($value === false) {
32+
throw new DeserializeException(
33+
code: DeserializeException::INVALID_VALUE_ERROR_CODE
34+
);
35+
}
36+
37+
/** @var ClassContext<static> $context */
38+
$context = ClassContext::getInstance(new ReflectionClass(static::class));
39+
40+
$data = DeserializePipeline::createFromArray()
41+
->sendThenReturn(new DeserializePipelinePassable(
42+
classContext: $context,
43+
data: $value
44+
))
45+
->data;
46+
47+
return static::instanceWithConstructorCallFrom($context, $data);
48+
} catch (Throwable $e) {
49+
throw DataCreationException::unableToCreateInstance(static::class, $e);
50+
}
51+
}
52+
2153
/**
2254
* @throws DeserializeException
2355
*/
@@ -26,7 +58,7 @@ final public static function from(mixed $value): static
2658
try {
2759
$value = static::normalizeValue($value, static::class);
2860

29-
if (empty($value)) {
61+
if ($value === false) {
3062
throw new DeserializeException(
3163
code: DeserializeException::INVALID_VALUE_ERROR_CODE
3264
);
@@ -35,10 +67,7 @@ final public static function from(mixed $value): static
3567
/** @var ClassContext<static> $context */
3668
$context = ClassContext::getInstance(new ReflectionClass(static::class));
3769

38-
$data = new Pipeline(DeserializePipelinePassable::class)
39-
->through(ResolveValuesFromAliasesPipe::class)
40-
->through(RefineDataPipe::class)
41-
->through(DecipherDataPipe::class)
70+
$data = DeserializePipeline::hydrateFromArray()
4271
->sendThenReturn(new DeserializePipelinePassable(
4372
classContext: $context,
4473
data: $value

src/Contexts/PropertyContext.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Nuxtifyts\PhpDto\Attributes\Property\Aliases;
66
use Nuxtifyts\PhpDto\Attributes\Property\CipherTarget;
77
use Nuxtifyts\PhpDto\Attributes\Property\Computed;
8+
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
89
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
910
use Nuxtifyts\PhpDto\DataCiphers\CipherConfig;
1011
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
@@ -13,6 +14,7 @@
1314
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
1415
use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException;
1516
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
17+
use Nuxtifyts\PhpDto\FallbackResolver\FallbackConfig;
1618
use Nuxtifyts\PhpDto\Serializers\Serializer;
1719
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
1820
use Nuxtifyts\PhpDto\Support\Traits\HasTypes;
@@ -42,6 +44,8 @@ class PropertyContext
4244

4345
private(set) ?CipherConfig $cipherConfig = null;
4446

47+
private(set) ?FallbackConfig $fallbackConfig = null;
48+
4549
/** @var list<DataRefiner> */
4650
private(set) array $dataRefiners = [];
4751

@@ -112,6 +116,16 @@ private function syncPropertyAttributes(): void
112116
encoded: $instance->encoded
113117
);
114118
}
119+
120+
if ($defaultsToAttribute = $this->reflection->getAttributes(DefaultsTo::class)[0] ?? null) {
121+
/** @var ReflectionAttribute<DefaultsTo> $defaultsToAttribute */
122+
$instance = $defaultsToAttribute->newInstance();
123+
124+
$this->fallbackConfig = new FallbackConfig(
125+
value: $instance->value,
126+
resolverClass: $instance->fallbackResolverClass
127+
);
128+
}
115129
}
116130

117131
public function getValue(object $object): mixed

src/Contracts/BaseData.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
namespace Nuxtifyts\PhpDto\Contracts;
44

5+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
56
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
67
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
78
use JsonSerializable;
89

910
interface BaseData extends JsonSerializable
1011
{
12+
/**
13+
* @throws DataCreationException
14+
*/
15+
public static function create(mixed ...$args): static;
16+
1117
/**
1218
* @return array<string, mixed>
1319
*
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Exceptions;
4+
5+
use Exception;
6+
use Throwable;
7+
8+
class DataCreationException extends Exception
9+
{
10+
protected const int UNABLE_TO_CREATE_INSTANCE = 0;
11+
protected const int INVALID_PROPERTY = 1;
12+
13+
public static function unableToCreateInstance(
14+
string $class,
15+
?Throwable $previous = null
16+
): self {
17+
return new self(
18+
message: "Unable to create instance of class {$class}",
19+
code: self::UNABLE_TO_CREATE_INSTANCE,
20+
previous: $previous
21+
);
22+
}
23+
24+
public static function invalidProperty(): self
25+
{
26+
return new self(
27+
message: 'Invalid property passed to create method',
28+
code: self::INVALID_PROPERTY
29+
);
30+
}
31+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Exceptions;
4+
5+
use Exception;
6+
7+
class FallbackResolverException extends Exception
8+
{
9+
protected const int UNABLE_TO_FIND_RESOLVER_CLASS = 0;
10+
protected const int UNABLE_TO_RESOLVE_DEFAULT_VALUE = 1;
11+
12+
public static function unableToFindResolverClass(string $resolverClass): self
13+
{
14+
return new self(
15+
"Unable to find resolver class: {$resolverClass}",
16+
self::UNABLE_TO_FIND_RESOLVER_CLASS
17+
);
18+
}
19+
20+
public static function unableToResolveDefaultValue(): self
21+
{
22+
return new self(
23+
'Unable to resolve default value',
24+
self::UNABLE_TO_RESOLVE_DEFAULT_VALUE
25+
);
26+
}
27+
}

0 commit comments

Comments
 (0)