Skip to content

Introducing Aliases Attribute #3

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 1 commit 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
Expand Down
24 changes: 24 additions & 0 deletions docs/PropertyAttributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-
Expand All @@ -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.
19 changes: 19 additions & 0 deletions src/Attributes/Property/Aliases.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Nuxtifyts\PhpDto\Attributes\Property;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Aliases
{
/** @var list<string> */
private(set) array $aliases;

public function __construct(
string $alias,
string ...$aliases
) {
$this->aliases = array_values([$alias, ...$aliases]);
}
}
3 changes: 2 additions & 1 deletion src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions src/Contexts/PropertyContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -29,6 +30,11 @@ class PropertyContext
*/
private static array $_instances = [];

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

private(set) bool $isComputed = false;

/** @var list<DataRefiner> */
Expand Down Expand Up @@ -85,6 +91,11 @@ private function syncPropertyAttributes(): void
/** @var ReflectionAttribute<WithRefiner> $withRefinerAttribute */
$this->dataRefiners[] = $withRefinerAttribute->newInstance()->getRefiner();
}

if ($aliasesAttribute = $this->reflection->getAttributes(Aliases::class)[0] ?? null) {
/** @var ReflectionAttribute<Aliases> $aliasesAttribute */
$this->aliases = $aliasesAttribute->newInstance()->aliases;
}
}

public function getValue(object $object): mixed
Expand Down
2 changes: 1 addition & 1 deletion src/Pipelines/DeserializePipeline/RefineDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public function handle(Passable $passable): DeserializePipelinePassable
}
}

return $passable->with($data);
return $passable->with(data: $data);
}
}
36 changes: 36 additions & 0 deletions src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Nuxtifyts\PhpDto\Pipelines\DeserializePipeline;

use Nuxtifyts\PhpDto\Support\Passable;
use Nuxtifyts\PhpDto\Support\Pipe;

/**
* @extends Pipe<DeserializePipelinePassable>
*/
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);
}
}
5 changes: 5 additions & 0 deletions tests/Dummies/PersonData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions tests/Unit/Attributes/AliasesAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Nuxtifyts\PhpDto\Tests\Unit\Attributes;

use Nuxtifyts\PhpDto\Attributes\Property\Aliases;
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe;
use Nuxtifyts\PhpDto\Tests\Dummies\PersonData;
use Nuxtifyts\PhpDto\Tests\Unit\UnitCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use Throwable;

#[CoversClass(PropertyContext::class)]
#[CoversClass(ResolveValuesFromAliasesPipe::class)]
#[CoversClass(Aliases::class)]
#[UsesClass(PersonData::class)]
final class AliasesAttributeTest extends UnitCase
{
/**
* @throws Throwable
*/
#[Test]
public function will_be_able_to_resolve_value_from_aliases(): void
{
$person = PersonData::from([
'first_name' => '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);
}
}
6 changes: 2 additions & 4 deletions tests/Unit/Concerns/BaseDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit/Serializers/ArraySerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Loading