Skip to content

Commit 80d5639

Browse files
authored
Merge pull request #2 from nuxtifyts/feature/data-refiner
Introducing data refiners
2 parents 295c651 + 3e30e09 commit 80d5639

File tree

22 files changed

+665
-27
lines changed

22 files changed

+665
-27
lines changed

docs/DataRefiners.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
Data Refiners
2+
=
3+
4+
In the deserialization process, sometimes we may need to refine the data before it is passed to the deserializer.
5+
This is where Data Refiners come in.
6+
7+
A pretty good example would be DateTimes. When attempting to create an instanceof DateTime, we may need to
8+
be aware of specific formats that the DateTime can be created from.
9+
10+
By default, these are the DataRefiners that are available in the library:
11+
- [DateTimeRefiner](#DateTimeRefiner) - Refines the data to a DateTimeImmutable instance depending on the format provided.
12+
13+
DateTimeRefiner
14+
-
15+
16+
```php
17+
use Nuxtifyts\PhpDto\Data;
18+
use DateTimeImmutable;
19+
20+
final readonly class DateRangeData extends Data
21+
{
22+
public function __construct(
23+
public ?DateTimeImmutable $start,
24+
public ?DateTimeImmutable $end
25+
) {}
26+
}
27+
```
28+
29+
With this DTO, if we try to hydrate it with a custom format `'Y/m-d'`, it will fail.
30+
31+
```php
32+
DateRangeData::from([
33+
'start' => '2023/01-12',
34+
'end' => '2023/01-14'
35+
]);
36+
```
37+
38+
To resolve this, we may need to specify a Data Refiner that will help deserialize the data.
39+
40+
```php
41+
use Nuxtifyts\PhpDto\Data;
42+
use DateTimeImmutable;
43+
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
44+
use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner;
45+
46+
final readonly class DateRangeData extends Data
47+
{
48+
public function __construct(
49+
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
50+
public ?DateTimeImmutable $start,
51+
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
52+
public ?DateTimeImmutable $end
53+
) {}
54+
}
55+
```
56+
57+
With this, hydrating the DTO will be possible.
58+
59+
Creating a Custom Data Refiner
60+
=
61+
62+
To create a custom Data Refiner, you need to implement the `DataRefiner` interface. for example suppose we
63+
want to create a Data Refiner that will refine an object of class `CustomDate`:
64+
65+
```php
66+
class CustomDate {
67+
public function __construct(
68+
private(set) int $year,
69+
private(set) int $month,
70+
private(set) int $day
71+
) {}
72+
73+
// ...
74+
}
75+
```
76+
77+
We can add the ability to hydrate a `DateTime` property from this class using a custom refiner like so:
78+
79+
```php
80+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
81+
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
82+
83+
84+
class CustomDateRefiner implements DataRefiner
85+
{
86+
public function refine(mixed $value, PropertyContext $property) : mixed
87+
{
88+
if ($value instanceof CustomDate) {
89+
return DateTimeImmutable::createFromFormat(
90+
format: 'Y-m-d',
91+
datetime: sprintf('%d-%d-%d', $value->year, $value->month, $value->day)
92+
);
93+
}
94+
95+
return $value;
96+
}
97+
}
98+
```
99+
100+
Now we can use this refiner in our previous DTO:
101+
102+
```php
103+
use Nuxtifyts\PhpDto\Data;
104+
use DateTimeImmutable;
105+
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
106+
use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner;
107+
108+
final readonly class DateRangeData extends Data
109+
{
110+
public function __construct(
111+
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
112+
#[WithRefiner(CustomDateRefiner::class)]
113+
public ?DateTimeImmutable $start,
114+
#[WithRefiner(CustomDateRefiner::class)]
115+
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
116+
public ?DateTimeImmutable $end
117+
) {}
118+
}
119+
```

docs/Quickstart.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,4 @@ can be found here:
7979
- [Supported Types](https://github.com/nuxtifyts/php-dto/blob/main/docs/SupportedTypes.md)
8080
- [Normalizers](https://github.com/nuxtifyts/php-dto/blob/main/docs/Normalizers.md)
8181
- [Property Attributes](https://github.com/nuxtifyts/php-dto/blob/main/docs/PropertyAttributes.md)
82+
- [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Attributes\Property;
4+
5+
use Attribute;
6+
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
7+
8+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
9+
class WithRefiner
10+
{
11+
/** @var array<array-key, mixed> */
12+
private array $refinerArgs;
13+
14+
/**
15+
* @param class-string<DataRefiner> $refinerClass
16+
*/
17+
public function __construct(
18+
private readonly string $refinerClass,
19+
mixed ...$refinerArgs
20+
) {
21+
$this->refinerArgs = $refinerArgs;
22+
}
23+
24+
public function getRefiner(): DataRefiner
25+
{
26+
return new $this->refinerClass(...$this->refinerArgs);
27+
}
28+
}

src/Concerns/BaseData.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace Nuxtifyts\PhpDto\Concerns;
44

55
use Nuxtifyts\PhpDto\Contexts\ClassContext;
6+
use Nuxtifyts\PhpDto\Data;
67
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
78
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
9+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
10+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe;
11+
use Nuxtifyts\PhpDto\Support\Pipeline;
812
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
913
use ReflectionClass;
1014
use Throwable;
@@ -30,9 +34,17 @@ final public static function from(mixed $value): static
3034
/** @var ClassContext<static> $context */
3135
$context = ClassContext::getInstance(new ReflectionClass(static::class));
3236

37+
$data = new Pipeline(DeserializePipelinePassable::class)
38+
->through(RefineDataPipe::class)
39+
->sendThenReturn(new DeserializePipelinePassable(
40+
classContext: $context,
41+
data: $value
42+
))
43+
->data;
44+
3345
return $context->hasComputedProperties
34-
? static::instanceWithConstructorCallFrom($context, $value)
35-
: static::instanceWithoutConstructorFrom($context, $value);
46+
? static::instanceWithConstructorCallFrom($context, $data)
47+
: static::instanceWithoutConstructorFrom($context, $data);
3648
} catch (Throwable $e) {
3749
throw new DeserializeException($e->getMessage(), $e->getCode(), $e);
3850
}

src/Contexts/ClassContext.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ class ClassContext
2929
public readonly array $constructorParams;
3030

3131
/**
32-
* @param ReflectionClass<T> $_reflectionClass
32+
* @param ReflectionClass<T> $reflection
3333
*
3434
* @throws UnsupportedTypeException
3535
*/
3636
final private function __construct(
37-
protected readonly ReflectionClass $_reflectionClass
37+
protected readonly ReflectionClass $reflection
3838
) {
39-
$this->properties = self::getPropertyContexts($this->_reflectionClass);
39+
$this->properties = self::getPropertyContexts($this->reflection);
4040
$this->constructorParams = array_map(
4141
static fn (ReflectionParameter $param) => $param->getName(),
42-
$this->_reflectionClass->getConstructor()?->getParameters() ?? [],
42+
$this->reflection->getConstructor()?->getParameters() ?? [],
4343
);
4444
}
4545

@@ -96,14 +96,14 @@ private static function getPropertyContexts(ReflectionClass $reflectionClass): a
9696
*/
9797
public function newInstanceWithoutConstructor(): mixed
9898
{
99-
return $this->_reflectionClass->newInstanceWithoutConstructor();
99+
return $this->reflection->newInstanceWithoutConstructor();
100100
}
101101

102102
/**
103103
* @throws ReflectionException
104104
*/
105105
public function newInstanceWithConstructorCall(mixed ...$args): mixed
106106
{
107-
return $this->_reflectionClass->newInstance(...$args);
107+
return $this->reflection->newInstance(...$args);
108108
}
109109
}

src/Contexts/PropertyContext.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Nuxtifyts\PhpDto\Contexts;
44

55
use Nuxtifyts\PhpDto\Attributes\Property\Computed;
6+
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
7+
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
68
use Nuxtifyts\PhpDto\Enums\Property\Type;
79
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
810
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
@@ -12,6 +14,7 @@
1214
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
1315
use Nuxtifyts\PhpDto\Support\Traits\HasTypes;
1416
use ReflectionProperty;
17+
use ReflectionAttribute;
1518

1619
class PropertyContext
1720
{
@@ -28,22 +31,25 @@ class PropertyContext
2831

2932
private(set) bool $isComputed = false;
3033

34+
/** @var list<DataRefiner> */
35+
private(set) array $dataRefiners = [];
36+
3137
/**
3238
* @throws UnsupportedTypeException
3339
*/
3440
final private function __construct(
35-
protected readonly ReflectionProperty $_reflectionProperty
41+
protected(set) readonly ReflectionProperty $reflection
3642
) {
37-
$this->syncTypesFromReflectionProperty($this->_reflectionProperty);
43+
$this->syncTypesFromReflectionProperty($this->reflection);
3844
$this->syncPropertyAttributes();
3945
}
4046

4147
public string $propertyName {
42-
get => $this->_reflectionProperty->getName();
48+
get => $this->reflection->getName();
4349
}
4450

4551
public string $className {
46-
get => $this->_reflectionProperty->getDeclaringClass()->getName();
52+
get => $this->reflection->getDeclaringClass()->getName();
4753
}
4854

4955
/** @var list<TypeContext<Type>> $arrayTypeContexts */
@@ -73,12 +79,17 @@ private static function getKey(ReflectionProperty $property): string
7379

7480
private function syncPropertyAttributes(): void
7581
{
76-
$this->isComputed = !empty($this->_reflectionProperty->getAttributes(Computed::class));
82+
$this->isComputed = !empty($this->reflection->getAttributes(Computed::class));
83+
84+
foreach ($this->reflection->getAttributes(WithRefiner::class) as $withRefinerAttribute) {
85+
/** @var ReflectionAttribute<WithRefiner> $withRefinerAttribute */
86+
$this->dataRefiners[] = $withRefinerAttribute->newInstance()->getRefiner();
87+
}
7788
}
7889

7990
public function getValue(object $object): mixed
8091
{
81-
return $this->_reflectionProperty->getValue($object);
92+
return $this->reflection->getValue($object);
8293
}
8394

8495
/**

src/Contexts/TypeContext.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use DateTimeInterface;
66
use Nuxtifyts\PhpDto\Data;
77
use Nuxtifyts\PhpDto\Enums\Property\Type;
8-
use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException;
98
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
109
use Nuxtifyts\PhpDto\Serializers\Serializer;
1110
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
@@ -64,9 +63,9 @@ final protected function __construct(
6463
*
6564
* @throws UnsupportedTypeException
6665
*/
67-
public static function getInstances(ReflectionProperty $property): array
66+
public static function getInstances(PropertyContext $property): array
6867
{
69-
$reflectionTypes = self::getPropertyStringTypes($property);
68+
$reflectionTypes = self::getPropertyStringTypes($property->reflection);
7069
$instances = [];
7170

7271
foreach ($reflectionTypes as $type) {
@@ -106,7 +105,7 @@ public static function getInstances(ReflectionProperty $property): array
106105
case $type === 'array':
107106
$instances[] = new static(
108107
Type::ARRAY,
109-
subTypeContexts: self::resolveSubContextsForArray($property)
108+
subTypeContexts: self::resolveSubContextsForArray($property->reflection)
110109
);
111110
break;
112111
default:

src/DataRefiners/DataRefiner.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\DataRefiners;
4+
5+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
6+
use Nuxtifyts\PhpDto\Enums\Property\Type;
7+
use Nuxtifyts\PhpDto\Exceptions\InvalidRefiner;
8+
9+
interface DataRefiner
10+
{
11+
/**
12+
* @throws InvalidRefiner
13+
*/
14+
public function refine(mixed $value, PropertyContext $property): mixed;
15+
}

0 commit comments

Comments
 (0)