Skip to content

Commit b9a89de

Browse files
gturpin-devbrendt
andauthored
feat(mapper): add MapFrom and MapTo attributes (#929)
Co-authored-by: brendt <[email protected]>
1 parent 964d55a commit b9a89de

File tree

11 files changed

+274
-2
lines changed

11 files changed

+274
-2
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Mapper\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_PROPERTY)]
10+
final readonly class MapFrom
11+
{
12+
public function __construct(
13+
public string $name,
14+
) {
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Mapper\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_PROPERTY)]
10+
final readonly class MapTo
11+
{
12+
public function __construct(
13+
public string $name,
14+
) {
15+
}
16+
}

src/Tempest/Mapper/src/Mappers/ArrayToObjectMapper.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tempest\Mapper\Mappers;
66

7+
use Tempest\Mapper\Attributes\MapFrom;
78
use Tempest\Mapper\Casters\ArrayCaster;
89
use Tempest\Mapper\Casters\CasterFactory;
910
use Tempest\Mapper\Exceptions\MissingValuesException;
@@ -57,7 +58,7 @@ public function map(mixed $from, mixed $to): object
5758
continue;
5859
}
5960

60-
$propertyName = $property->getName();
61+
$propertyName = $this->resolvePropertyName($property);
6162

6263
if (! array_key_exists($propertyName, $from)) {
6364
$isStrictProperty = $isStrictClass || $property->hasAttribute(Strict::class);
@@ -117,6 +118,17 @@ public function map(mixed $from, mixed $to): object
117118
return $object;
118119
}
119120

121+
private function resolvePropertyName(PropertyReflector $property): string
122+
{
123+
$mapFrom = $property->getAttribute(MapFrom::class);
124+
125+
if ($mapFrom !== null) {
126+
return $mapFrom->name;
127+
}
128+
129+
return $property->getName();
130+
}
131+
120132
private function resolveObject(mixed $objectOrClass): object
121133
{
122134
if (is_object($objectOrClass)) {

src/Tempest/Mapper/src/Mappers/ObjectToArrayMapper.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
namespace Tempest\Mapper\Mappers;
66

77
use JsonSerializable;
8+
use ReflectionException;
9+
use Tempest\Mapper\Attributes\MapTo as MapToAttribute;
810
use Tempest\Mapper\Mapper;
911
use Tempest\Mapper\MapTo;
12+
use Tempest\Reflection\ClassReflector;
13+
use Tempest\Reflection\PropertyReflector;
14+
use function Tempest\Support\arr;
1015

1116
final readonly class ObjectToArrayMapper implements Mapper
1217
{
@@ -16,11 +21,54 @@ public function canMap(mixed $from, mixed $to): bool
1621
}
1722

1823
public function map(mixed $from, mixed $to): array
24+
{
25+
$properties = $this->resolveProperties($from);
26+
$mappedProperties = [];
27+
28+
foreach ($properties as $propertyName => $propertyValue) {
29+
try {
30+
$property = PropertyReflector::fromParts(class: $from, name: $propertyName);
31+
32+
$propertyName = $this->resolvePropertyName($property);
33+
34+
$mappedProperties[$propertyName] = $propertyValue;
35+
} catch (ReflectionException) {
36+
continue;
37+
}
38+
}
39+
40+
return $mappedProperties;
41+
}
42+
43+
/**
44+
* @return array<string, mixed> The properties name and value
45+
*/
46+
private function resolveProperties(object $from): array
1947
{
2048
if ($from instanceof JsonSerializable) {
2149
return $from->jsonSerialize();
2250
}
2351

24-
return (array) $from;
52+
try {
53+
$class = new ClassReflector($from);
54+
$properties = $class->getPublicProperties();
55+
56+
return arr(iterator_to_array($properties))
57+
->mapWithKeys(fn (PropertyReflector $property) => yield $property->getName() => $property->getValue($from))
58+
->toArray();
59+
} catch (ReflectionException) {
60+
return [];
61+
}
62+
}
63+
64+
private function resolvePropertyName(PropertyReflector $property): string
65+
{
66+
$mapTo = $property->getAttribute(MapToAttribute::class);
67+
68+
if ($mapTo !== null) {
69+
return $mapTo->name;
70+
}
71+
72+
return $property->getName();
2573
}
2674
}

src/Tempest/Reflection/src/PropertyReflector.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ public function isNullable(): bool
7171
return $this->getType()->isNullable();
7272
}
7373

74+
public function isPrivate(): bool
75+
{
76+
return $this->reflectionProperty->isPrivate();
77+
}
78+
79+
public function isProtected(): bool
80+
{
81+
return $this->reflectionProperty->isProtected();
82+
}
83+
84+
public function isPublic(): bool
85+
{
86+
return $this->reflectionProperty->isPublic();
87+
}
88+
7489
public function getIterableType(): ?TypeReflector
7590
{
7691
$doc = $this->reflectionProperty->getDocComment();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Mapper\Fixtures;
6+
7+
use Tempest\Mapper\Attributes\MapFrom;
8+
9+
final class ObjectWithMapFromAttribute
10+
{
11+
public function __construct(
12+
#[MapFrom('name')]
13+
public readonly string $fullName,
14+
) {
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Mapper\Fixtures;
6+
7+
use Tempest\Mapper\Attributes\MapTo;
8+
9+
final class ObjectWithMapToAttribute
10+
{
11+
public function __construct(
12+
#[MapTo('name')]
13+
public readonly string $fullName,
14+
) {
15+
}
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Mapper\Fixtures;
6+
7+
use Tempest\Mapper\Attributes\MapTo;
8+
9+
final class ObjectWithMapToCollisions
10+
{
11+
public function __construct(
12+
#[MapTo('name')]
13+
public readonly string $first_name,
14+
#[MapTo('full_name')]
15+
public readonly string $name,
16+
public readonly string $last_name,
17+
) {
18+
}
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Mapper\Fixtures;
6+
7+
use JsonSerializable;
8+
use Tempest\Mapper\Attributes\MapTo;
9+
10+
final class ObjectWithMapToCollisionsJsonSerializable implements JsonSerializable
11+
{
12+
public function __construct(
13+
#[MapTo('name')]
14+
public readonly string $first_name,
15+
#[MapTo('full_name')]
16+
public readonly string $name,
17+
public readonly string $last_name,
18+
) {
19+
}
20+
21+
public function jsonSerialize(): mixed
22+
{
23+
return [
24+
'first_name' => $this->first_name,
25+
'name' => $this->name,
26+
];
27+
}
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Mapper\Fixtures;
6+
7+
use Tempest\Mapper\Attributes\MapTo;
8+
9+
final class ObjectWithMappedVariousPropertyScope
10+
{
11+
public function __construct(
12+
#[MapTo('private')] // @phpstan-ignore-line property.onlyWritten
13+
private string $privateProp,
14+
#[MapTo('protected')]
15+
protected string $protectedProp,
16+
#[MapTo('public')]
17+
public string $publicProp,
18+
) {
19+
}
20+
}

0 commit comments

Comments
 (0)