Skip to content

Commit e3a305a

Browse files
committed
Add support for creating DTO instances using a create method
Introduces a `create` method to support default values for complex types, leveraging a new streamlined `DeserializePipeline`. Adjustments include a new `DataCreationException`, documentation updates, fallback resolver enhancements, and pipeline refactoring for better organization and maintainability.
1 parent f02b3c5 commit e3a305a

18 files changed

+315
-49
lines changed

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: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,27 +157,5 @@ final readonly class User extends Data
157157
The `DefaultsTo` attribute provides the ability to specify default values for complex types,
158158
such as DateTimes and DTOs.
159159

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-
160+
For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md)
161+
guide.

src/Concerns/BaseData.php

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +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\ResolveDefaultDataPipe;
12-
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe;
13-
use Nuxtifyts\PhpDto\Support\Pipeline;
1411
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
1512
use ReflectionClass;
1613
use Throwable;
@@ -19,6 +16,40 @@ trait BaseData
1916
{
2017
use HasNormalizers;
2118

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+
2253
/**
2354
* @throws DeserializeException
2455
*/
@@ -36,11 +67,7 @@ final public static function from(mixed $value): static
3667
/** @var ClassContext<static> $context */
3768
$context = ClassContext::getInstance(new ReflectionClass(static::class));
3869

39-
$data = new Pipeline(DeserializePipelinePassable::class)
40-
->through(ResolveValuesFromAliasesPipe::class)
41-
->through(RefineDataPipe::class)
42-
->through(DecipherDataPipe::class)
43-
->through(ResolveDefaultDataPipe::class)
70+
$data = DeserializePipeline::hydrateFromArray()
4471
->sendThenReturn(new DeserializePipelinePassable(
4572
classContext: $context,
4673
data: $value

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
4+
5+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\DecipherDataPipe;
6+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\RefineDataPipe;
7+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\ResolveDefaultDataPipe;
8+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\ResolveValuesFromAliasesPipe;
9+
use Nuxtifyts\PhpDto\Support\Pipeline;
10+
11+
/**
12+
* @extends Pipeline<DeserializePipelinePassable>
13+
*/
14+
class DeserializePipeline extends Pipeline
15+
{
16+
public static function hydrateFromArray(): self
17+
{
18+
return new DeserializePipeline(DeserializePipelinePassable::class)
19+
->through(ResolveValuesFromAliasesPipe::class)
20+
->through(RefineDataPipe::class)
21+
->through(DecipherDataPipe::class)
22+
->through(ResolveDefaultDataPipe::class);
23+
}
24+
25+
/**
26+
* @desc Basically the same as hydrateFromArray, but without deciphering data.
27+
* This is used when create a new instance using the `create` static method from `BaseData`.
28+
*/
29+
public static function createFromArray(): self
30+
{
31+
return new DeserializePipeline(DeserializePipelinePassable::class)
32+
->through(ResolveValuesFromAliasesPipe::class)
33+
->through(RefineDataPipe::class)
34+
->through(ResolveDefaultDataPipe::class);
35+
}
36+
}

src/Pipelines/DeserializePipeline/DecipherDataPipe.php renamed to src/Pipelines/DeserializePipeline/Pipes/DecipherDataPipe.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php
22

3-
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes;
44

5+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
56
use Nuxtifyts\PhpDto\Support\Passable;
67
use Nuxtifyts\PhpDto\Support\Pipe;
78

src/Pipelines/DeserializePipeline/RefineDataPipe.php renamed to src/Pipelines/DeserializePipeline/Pipes/RefineDataPipe.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<?php
22

3-
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes;
44

5+
use Exception;
6+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
57
use Nuxtifyts\PhpDto\Support\Passable;
68
use Nuxtifyts\PhpDto\Support\Pipe;
7-
use Exception;
89

910
/**
1011
* @extends Pipe<DeserializePipelinePassable>

src/Pipelines/DeserializePipeline/ResolveDefaultDataPipe.php renamed to src/Pipelines/DeserializePipeline/Pipes/ResolveDefaultDataPipe.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php
22

3-
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes;
44

5+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
56
use Nuxtifyts\PhpDto\Support\Passable;
67
use Nuxtifyts\PhpDto\Support\Pipe;
78
use ReflectionParameter;

src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php renamed to src/Pipelines/DeserializePipeline/Pipes/ResolveValuesFromAliasesPipe.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php
22

3-
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes;
44

5+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
56
use Nuxtifyts\PhpDto\Support\Passable;
67
use Nuxtifyts\PhpDto\Support\Pipe;
78

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\Tests\Dummies\PointData;
6+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
7+
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;
8+
9+
class DummyPointsFallbackResolver implements FallbackResolver
10+
{
11+
/**
12+
* @return list<PointData>
13+
*/
14+
public static function resolve(array $rawData, PropertyContext $property): array
15+
{
16+
return [
17+
new PointData(1, 2),
18+
new PointData(3, 4),
19+
new PointData(5, 6),
20+
];
21+
}
22+
}

tests/Dummies/PointData.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Dummies;
4+
5+
use Nuxtifyts\PhpDto\Data;
6+
7+
final readonly class PointData extends Data
8+
{
9+
public function __construct(
10+
public float $x,
11+
public float $y
12+
) {
13+
}
14+
}

tests/Dummies/PointGroupData.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Dummies;
4+
5+
use Nuxtifyts\PhpDto\Attributes\Property\CipherTarget;
6+
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
7+
use Nuxtifyts\PhpDto\Attributes\Property\Types\ArrayOfData;
8+
use Nuxtifyts\PhpDto\Data;
9+
use Nuxtifyts\PhpDto\Tests\Dummies\FallbackResolvers\DummyPointsFallbackResolver;
10+
11+
final readonly class PointGroupData extends Data
12+
{
13+
/**
14+
* @param array<array-key, PointData> $points
15+
*/
16+
public function __construct(
17+
#[CipherTarget]
18+
public string $key,
19+
#[ArrayOfData(PointData::class)]
20+
#[DefaultsTo(DummyPointsFallbackResolver::class)]
21+
public array $points
22+
) {
23+
}
24+
}

tests/Unit/Attributes/AliasesTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Nuxtifyts\PhpDto\Attributes\Property\Aliases;
66
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
7-
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe;
7+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\ResolveValuesFromAliasesPipe;
88
use Nuxtifyts\PhpDto\Tests\Dummies\PersonData;
99
use Nuxtifyts\PhpDto\Tests\Unit\UnitCase;
1010
use PHPUnit\Framework\Attributes\CoversClass;

0 commit comments

Comments
 (0)