diff --git a/README.md b/README.md index 124f98f..f1fd77b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Then you can define the properties of the class and their types. ```php use Nuxtifyts\PhpDto\Data; +use Nuxtifyts\PhpDto\Attributes\Property\Aliases; use Nuxtifyts\PhpDto\Attributes\Property\Computed; final readonly class UserData extends Data @@ -22,6 +23,7 @@ final readonly class UserData extends Data public function __construct( public string $firstName, + #[Aliases('familyName')] public stirng $lastName ) { $this->fullName = "$this->firstName $this->lastName"; diff --git a/docs/PropertyAttributes.md b/docs/PropertyAttributes.md index 1ffcba6..8a81c02 100644 --- a/docs/PropertyAttributes.md +++ b/docs/PropertyAttributes.md @@ -3,6 +3,7 @@ Property Attributes In order to provide more functionality to your DTOs, you can use the following attributes: - [Computed](#Computed) - To define a property that is computed from other properties. +- [Aliases](#Aliases) - To define aliases for a property. Computed - @@ -29,3 +30,26 @@ final readonly class Person extends Data ``` This will make the DTO aware of the `fullName` property, and it will not be serialized or deserialized. + +Aliases +- + +Sometimes, we may need to specify that a property can be hydrated from multiple keys in the data array. +This can be done using the `Aliases` attribute. + +```php +use Nuxtifyts\PhpDto\Data; +use Nuxtifyts\PhpDto\Attributes\Property\Aliases; + +final readonly class Person extends Data +{ + public function __construct( + #[Aliases('first_name')] + public string $firstName, + #[Aliases('last_name')] + public string $lastName + ) {} +} +``` + +This will make it possible to hydrate properties from multiple array keys. diff --git a/src/Attributes/Property/Aliases.php b/src/Attributes/Property/Aliases.php new file mode 100644 index 0000000..677d6a6 --- /dev/null +++ b/src/Attributes/Property/Aliases.php @@ -0,0 +1,19 @@ + */ + private(set) array $aliases; + + public function __construct( + string $alias, + string ...$aliases + ) { + $this->aliases = array_values([$alias, ...$aliases]); + } +} diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index c3a4338..ef2bf37 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -3,11 +3,11 @@ 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\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe; use Nuxtifyts\PhpDto\Support\Pipeline; use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers; use ReflectionClass; @@ -35,6 +35,7 @@ final public static function from(mixed $value): static $context = ClassContext::getInstance(new ReflectionClass(static::class)); $data = new Pipeline(DeserializePipelinePassable::class) + ->through(ResolveValuesFromAliasesPipe::class) ->through(RefineDataPipe::class) ->sendThenReturn(new DeserializePipelinePassable( classContext: $context, diff --git a/src/Contexts/PropertyContext.php b/src/Contexts/PropertyContext.php index 038811d..d3006a6 100644 --- a/src/Contexts/PropertyContext.php +++ b/src/Contexts/PropertyContext.php @@ -2,6 +2,7 @@ namespace Nuxtifyts\PhpDto\Contexts; +use Nuxtifyts\PhpDto\Attributes\Property\Aliases; use Nuxtifyts\PhpDto\Attributes\Property\Computed; use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner; use Nuxtifyts\PhpDto\DataRefiners\DataRefiner; @@ -29,6 +30,11 @@ class PropertyContext */ private static array $_instances = []; + /** + * @var list + */ + private(set) array $aliases = []; + private(set) bool $isComputed = false; /** @var list */ @@ -85,6 +91,11 @@ private function syncPropertyAttributes(): void /** @var ReflectionAttribute $withRefinerAttribute */ $this->dataRefiners[] = $withRefinerAttribute->newInstance()->getRefiner(); } + + if ($aliasesAttribute = $this->reflection->getAttributes(Aliases::class)[0] ?? null) { + /** @var ReflectionAttribute $aliasesAttribute */ + $this->aliases = $aliasesAttribute->newInstance()->aliases; + } } public function getValue(object $object): mixed diff --git a/src/Pipelines/DeserializePipeline/RefineDataPipe.php b/src/Pipelines/DeserializePipeline/RefineDataPipe.php index 33b0d5b..42c629c 100644 --- a/src/Pipelines/DeserializePipeline/RefineDataPipe.php +++ b/src/Pipelines/DeserializePipeline/RefineDataPipe.php @@ -32,6 +32,6 @@ public function handle(Passable $passable): DeserializePipelinePassable } } - return $passable->with($data); + return $passable->with(data: $data); } } diff --git a/src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php b/src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php new file mode 100644 index 0000000..55a0dcb --- /dev/null +++ b/src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php @@ -0,0 +1,36 @@ + + */ +readonly class ResolveValuesFromAliasesPipe extends Pipe +{ + public function handle(Passable $passable): DeserializePipelinePassable + { + $data = $passable->data; + + foreach ($passable->classContext->properties as $propertyContext) { + $propertyName = $propertyContext->propertyName; + + if (array_key_exists($propertyName, $data)) { + continue; + } + + $aliases = $propertyContext->aliases; + + foreach ($aliases as $alias) { + if (array_key_exists($alias, $data)) { + $data[$propertyName] = $data[$alias]; + break; + } + } + } + + return $passable->with(data: $data); + } +} diff --git a/tests/Dummies/PersonData.php b/tests/Dummies/PersonData.php index 5082663..ecbd8ad 100644 --- a/tests/Dummies/PersonData.php +++ b/tests/Dummies/PersonData.php @@ -2,14 +2,19 @@ namespace Nuxtifyts\PhpDto\Tests\Dummies; +use Nuxtifyts\PhpDto\Attributes\Property\Aliases; +use Nuxtifyts\PhpDto\Attributes\Property\Computed; use Nuxtifyts\PhpDto\Data; final readonly class PersonData extends Data { + #[Computed] public string $fullName; public function __construct( + #[Aliases('first_name', 'name')] public string $firstName, + #[Aliases('last_name', 'family_name')] public string $lastName, ) { $this->fullName = $this->firstName . ' ' . $this->lastName; diff --git a/tests/Unit/Attributes/AliasesAttributeTest.php b/tests/Unit/Attributes/AliasesAttributeTest.php new file mode 100644 index 0000000..bc15c68 --- /dev/null +++ b/tests/Unit/Attributes/AliasesAttributeTest.php @@ -0,0 +1,43 @@ + 'John', + 'last_name' => 'Doe' + ]); + + self::assertEquals('John', $person->firstName); + self::assertEquals('Doe', $person->lastName); + + $person = PersonData::from([ + 'first_name' => 'John', + 'family_name' => 'Doe' + ]); + + self::assertEquals('John', $person->firstName); + self::assertEquals('Doe', $person->lastName); + } +} diff --git a/tests/Unit/Concerns/BaseDataTest.php b/tests/Unit/Concerns/BaseDataTest.php index 0415430..687cc8f 100644 --- a/tests/Unit/Concerns/BaseDataTest.php +++ b/tests/Unit/Concerns/BaseDataTest.php @@ -67,8 +67,7 @@ public function base_data_supports_scalar_types(): void self::assertEquals( [ 'firstName' => 'John', - 'lastName' => 'Doe', - 'fullName' => 'John Doe' + 'lastName' => 'Doe' ], $personData = $person->jsonSerialize() ); @@ -171,8 +170,7 @@ public static function will_perform_serialization_and_deserialization_data_provi 'dtoClass' => PersonData::class, 'data' => $data = [ 'firstName' => 'John', - 'lastName' => 'Doe', - 'fullName' => 'John Doe' + 'lastName' => 'Doe' ], 'expectedProperties' => [ 'firstName' => 'John', diff --git a/tests/Unit/Serializers/ArraySerializerTest.php b/tests/Unit/Serializers/ArraySerializerTest.php index 449b99d..d14a6db 100644 --- a/tests/Unit/Serializers/ArraySerializerTest.php +++ b/tests/Unit/Serializers/ArraySerializerTest.php @@ -174,7 +174,7 @@ public static function will_perform_data_serialization_on_array_types_data_provi 'object' => new ArrayOfAttributesData(arrayOfPersonData: [new PersonData('John', 'Doe')]), 'expectedSerializedValue' => [ 'arrayOfPersonData' => [ - ['firstName' => 'John', 'lastName' => 'Doe', 'fullName' => 'John Doe'] + ['firstName' => 'John', 'lastName' => 'Doe'] ] ], 'propertyName' => 'arrayOfPersonData', @@ -209,8 +209,8 @@ public static function will_perform_data_serialization_on_array_types_data_provi ), 'expectedSerializedValue' => [ 'arrayOfPersonData' => [ - 'john-doe' => ['firstName' => 'John', 'lastName' => 'Doe', 'fullName' => 'John Doe'], - 'jane-doe' => ['firstName' => 'Jane', 'lastName' => 'Doe', 'fullName' => 'Jane Doe'], + 'john-doe' => ['firstName' => 'John', 'lastName' => 'Doe'], + 'jane-doe' => ['firstName' => 'Jane', 'lastName' => 'Doe'], ] ], 'propertyName' => 'arrayOfPersonData',