Skip to content

Commit 39b59fe

Browse files
committed
Added support for refiners
1 parent 295c651 commit 39b59fe

File tree

18 files changed

+506
-26
lines changed

18 files changed

+506
-26
lines changed
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+
}

src/DataRefiners/DateTimeRefiner.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\DataRefiners;
4+
5+
use DateTimeInterface;
6+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
7+
use Nuxtifyts\PhpDto\Enums\Property\Type;
8+
use Nuxtifyts\PhpDto\Exceptions\InvalidRefiner;
9+
use DateTimeImmutable;
10+
11+
class DateTimeRefiner implements DataRefiner
12+
{
13+
/** @var list<string> */
14+
protected(set) array $formats = [
15+
DateTimeInterface::ATOM,
16+
'Y-m-d H:i:s',
17+
'Y-m-d'
18+
];
19+
20+
/**
21+
* @param string|list<string>|null $formats
22+
*/
23+
public function __construct(
24+
string|array|null $formats = null
25+
) {
26+
if (!is_null($formats)) {
27+
$this->formats = is_string($formats) ? [$formats] : $formats;
28+
}
29+
}
30+
31+
public function refine(mixed $value, PropertyContext $property): mixed
32+
{
33+
if (is_null($value)) {
34+
return null;
35+
}
36+
37+
if (is_string($value)) {
38+
$typeContexts = $property->getFilteredTypeContexts(Type::DATETIME);
39+
40+
if (empty($typeContexts)) {
41+
throw InvalidRefiner::from($this, $property);
42+
}
43+
44+
$refinedValue = false;
45+
46+
if (array_any(
47+
$this->formats,
48+
static function(string $format) use (&$refinedValue, $value): bool {
49+
return (bool)($refinedValue = DateTimeImmutable::createFromFormat($format, $value));
50+
}
51+
)) {
52+
return $refinedValue;
53+
}
54+
}
55+
56+
return $value;
57+
}
58+
}

src/Exceptions/InvalidRefiner.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Exceptions;
4+
5+
use Exception;
6+
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
7+
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
8+
9+
class InvalidRefiner extends Exception
10+
{
11+
public static function from(
12+
DataRefiner $refiner,
13+
PropertyContext $property
14+
): self {
15+
return new self(
16+
sprintf(
17+
'Refiner %s is not applicable to property %s',
18+
get_class($refiner),
19+
$property->propertyName
20+
)
21+
);
22+
}
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
4+
5+
use Nuxtifyts\PhpDto\Contexts\ClassContext;
6+
use Nuxtifyts\PhpDto\Data;
7+
use Nuxtifyts\PhpDto\Support\Passable;
8+
9+
readonly class DeserializePipelinePassable extends Passable
10+
{
11+
/**
12+
* @template T of Data
13+
*
14+
* @param ClassContext<T> $classContext
15+
* @param array<string, mixed> $data
16+
*/
17+
public function __construct(
18+
protected(set) ClassContext $classContext,
19+
protected(set) array $data
20+
) {
21+
}
22+
23+
/**
24+
* @param array<string, mixed> $data
25+
*/
26+
public function with(array $data): DeserializePipelinePassable
27+
{
28+
return new self($this->classContext, $data);
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;
4+
5+
use Nuxtifyts\PhpDto\Support\Passable;
6+
use Nuxtifyts\PhpDto\Support\Pipe;
7+
use Exception;
8+
9+
/**
10+
* @extends Pipe<DeserializePipelinePassable>
11+
*/
12+
readonly class RefineDataPipe extends Pipe
13+
{
14+
public function handle(Passable $passable): DeserializePipelinePassable
15+
{
16+
$data = $passable->data;
17+
18+
foreach ($passable->classContext->properties as $propertyContext) {
19+
$propertyName = $propertyContext->propertyName;
20+
21+
if (!array_key_exists($propertyName, $data)) {
22+
continue;
23+
}
24+
25+
foreach ($propertyContext->dataRefiners as $dataRefiner) {
26+
try {
27+
$data[$propertyName] = $dataRefiner->refine(
28+
value: $data[$propertyName],
29+
property: $propertyContext
30+
);
31+
} catch (Exception) {}
32+
}
33+
}
34+
35+
return $passable->with($data);
36+
}
37+
}

0 commit comments

Comments
 (0)