Skip to content

Introducing data refiners #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions docs/DataRefiners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Data Refiners
=

In the deserialization process, sometimes we may need to refine the data before it is passed to the deserializer.
This is where Data Refiners come in.

A pretty good example would be DateTimes. When attempting to create an instanceof DateTime, we may need to
be aware of specific formats that the DateTime can be created from.

By default, these are the DataRefiners that are available in the library:
- [DateTimeRefiner](#DateTimeRefiner) - Refines the data to a DateTimeImmutable instance depending on the format provided.

DateTimeRefiner
-

```php
use Nuxtifyts\PhpDto\Data;
use DateTimeImmutable;

final readonly class DateRangeData extends Data
{
public function __construct(
public ?DateTimeImmutable $start,
public ?DateTimeImmutable $end
) {}
}
```

With this DTO, if we try to hydrate it with a custom format `'Y/m-d'`, it will fail.

```php
DateRangeData::from([
'start' => '2023/01-12',
'end' => '2023/01-14'
]);
```

To resolve this, we may need to specify a Data Refiner that will help deserialize the data.

```php
use Nuxtifyts\PhpDto\Data;
use DateTimeImmutable;
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner;

final readonly class DateRangeData extends Data
{
public function __construct(
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
public ?DateTimeImmutable $start,
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
public ?DateTimeImmutable $end
) {}
}
```

With this, hydrating the DTO will be possible.

Creating a Custom Data Refiner
=

To create a custom Data Refiner, you need to implement the `DataRefiner` interface. for example suppose we
want to create a Data Refiner that will refine an object of class `CustomDate`:

```php
class CustomDate {
public function __construct(
private(set) int $year,
private(set) int $month,
private(set) int $day
) {}

// ...
}
```

We can add the ability to hydrate a `DateTime` property from this class using a custom refiner like so:

```php
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;


class CustomDateRefiner implements DataRefiner
{
public function refine(mixed $value, PropertyContext $property) : mixed
{
if ($value instanceof CustomDate) {
return DateTimeImmutable::createFromFormat(
format: 'Y-m-d',
datetime: sprintf('%d-%d-%d', $value->year, $value->month, $value->day)
);
}

return $value;
}
}
```

Now we can use this refiner in our previous DTO:

```php
use Nuxtifyts\PhpDto\Data;
use DateTimeImmutable;
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner;

final readonly class DateRangeData extends Data
{
public function __construct(
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
#[WithRefiner(CustomDateRefiner::class)]
public ?DateTimeImmutable $start,
#[WithRefiner(CustomDateRefiner::class)]
#[WithRefiner(DateTimeRefiner::class, formats: 'Y/m-d')]
public ?DateTimeImmutable $end
) {}
}
```
1 change: 1 addition & 0 deletions docs/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ can be found here:
- [Supported Types](https://github.com/nuxtifyts/php-dto/blob/main/docs/SupportedTypes.md)
- [Normalizers](https://github.com/nuxtifyts/php-dto/blob/main/docs/Normalizers.md)
- [Property Attributes](https://github.com/nuxtifyts/php-dto/blob/main/docs/PropertyAttributes.md)
- [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md)
28 changes: 28 additions & 0 deletions src/Attributes/Property/WithRefiner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Nuxtifyts\PhpDto\Attributes\Property;

use Attribute;
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
class WithRefiner
{
/** @var array<array-key, mixed> */
private array $refinerArgs;

/**
* @param class-string<DataRefiner> $refinerClass
*/
public function __construct(
private readonly string $refinerClass,
mixed ...$refinerArgs
) {
$this->refinerArgs = $refinerArgs;
}

public function getRefiner(): DataRefiner
{
return new $this->refinerClass(...$this->refinerArgs);
}
}
16 changes: 14 additions & 2 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
namespace Nuxtifyts\PhpDto\Concerns;

use Nuxtifyts\PhpDto\Contexts\ClassContext;
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe;
use Nuxtifyts\PhpDto\Support\Pipeline;
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
use ReflectionClass;
use Throwable;
Expand All @@ -30,9 +34,17 @@ final public static function from(mixed $value): static
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(new ReflectionClass(static::class));

$data = new Pipeline(DeserializePipelinePassable::class)
->through(RefineDataPipe::class)
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return $context->hasComputedProperties
? static::instanceWithConstructorCallFrom($context, $value)
: static::instanceWithoutConstructorFrom($context, $value);
? static::instanceWithConstructorCallFrom($context, $data)
: static::instanceWithoutConstructorFrom($context, $data);
} catch (Throwable $e) {
throw new DeserializeException($e->getMessage(), $e->getCode(), $e);
}
Expand Down
12 changes: 6 additions & 6 deletions src/Contexts/ClassContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ class ClassContext
public readonly array $constructorParams;

/**
* @param ReflectionClass<T> $_reflectionClass
* @param ReflectionClass<T> $reflection
*
* @throws UnsupportedTypeException
*/
final private function __construct(
protected readonly ReflectionClass $_reflectionClass
protected readonly ReflectionClass $reflection
) {
$this->properties = self::getPropertyContexts($this->_reflectionClass);
$this->properties = self::getPropertyContexts($this->reflection);
$this->constructorParams = array_map(
static fn (ReflectionParameter $param) => $param->getName(),
$this->_reflectionClass->getConstructor()?->getParameters() ?? [],
$this->reflection->getConstructor()?->getParameters() ?? [],
);
}

Expand Down Expand Up @@ -96,14 +96,14 @@ private static function getPropertyContexts(ReflectionClass $reflectionClass): a
*/
public function newInstanceWithoutConstructor(): mixed
{
return $this->_reflectionClass->newInstanceWithoutConstructor();
return $this->reflection->newInstanceWithoutConstructor();
}

/**
* @throws ReflectionException
*/
public function newInstanceWithConstructorCall(mixed ...$args): mixed
{
return $this->_reflectionClass->newInstance(...$args);
return $this->reflection->newInstance(...$args);
}
}
23 changes: 17 additions & 6 deletions src/Contexts/PropertyContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Nuxtifyts\PhpDto\Contexts;

use Nuxtifyts\PhpDto\Attributes\Property\Computed;
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
use Nuxtifyts\PhpDto\Enums\Property\Type;
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
Expand All @@ -12,6 +14,7 @@
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
use Nuxtifyts\PhpDto\Support\Traits\HasTypes;
use ReflectionProperty;
use ReflectionAttribute;

class PropertyContext
{
Expand All @@ -28,22 +31,25 @@ class PropertyContext

private(set) bool $isComputed = false;

/** @var list<DataRefiner> */
private(set) array $dataRefiners = [];

/**
* @throws UnsupportedTypeException
*/
final private function __construct(
protected readonly ReflectionProperty $_reflectionProperty
protected(set) readonly ReflectionProperty $reflection
) {
$this->syncTypesFromReflectionProperty($this->_reflectionProperty);
$this->syncTypesFromReflectionProperty($this->reflection);
$this->syncPropertyAttributes();
}

public string $propertyName {
get => $this->_reflectionProperty->getName();
get => $this->reflection->getName();
}

public string $className {
get => $this->_reflectionProperty->getDeclaringClass()->getName();
get => $this->reflection->getDeclaringClass()->getName();
}

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

private function syncPropertyAttributes(): void
{
$this->isComputed = !empty($this->_reflectionProperty->getAttributes(Computed::class));
$this->isComputed = !empty($this->reflection->getAttributes(Computed::class));

foreach ($this->reflection->getAttributes(WithRefiner::class) as $withRefinerAttribute) {
/** @var ReflectionAttribute<WithRefiner> $withRefinerAttribute */
$this->dataRefiners[] = $withRefinerAttribute->newInstance()->getRefiner();
}
}

public function getValue(object $object): mixed
{
return $this->_reflectionProperty->getValue($object);
return $this->reflection->getValue($object);
}

/**
Expand Down
7 changes: 3 additions & 4 deletions src/Contexts/TypeContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use DateTimeInterface;
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Enums\Property\Type;
use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException;
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
use Nuxtifyts\PhpDto\Serializers\Serializer;
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
Expand Down Expand Up @@ -64,9 +63,9 @@ final protected function __construct(
*
* @throws UnsupportedTypeException
*/
public static function getInstances(ReflectionProperty $property): array
public static function getInstances(PropertyContext $property): array
{
$reflectionTypes = self::getPropertyStringTypes($property);
$reflectionTypes = self::getPropertyStringTypes($property->reflection);
$instances = [];

foreach ($reflectionTypes as $type) {
Expand Down Expand Up @@ -106,7 +105,7 @@ public static function getInstances(ReflectionProperty $property): array
case $type === 'array':
$instances[] = new static(
Type::ARRAY,
subTypeContexts: self::resolveSubContextsForArray($property)
subTypeContexts: self::resolveSubContextsForArray($property->reflection)
);
break;
default:
Expand Down
15 changes: 15 additions & 0 deletions src/DataRefiners/DataRefiner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Nuxtifyts\PhpDto\DataRefiners;

use Nuxtifyts\PhpDto\Contexts\PropertyContext;
use Nuxtifyts\PhpDto\Enums\Property\Type;
use Nuxtifyts\PhpDto\Exceptions\InvalidRefiner;

interface DataRefiner
{
/**
* @throws InvalidRefiner
*/
public function refine(mixed $value, PropertyContext $property): mixed;
}
Loading
Loading