Skip to content

Commit 5ba9880

Browse files
authored
Merge pull request #13 from nuxtifyts/feature/with-normalizers-and-with-serializer-attributes
Add support for normalizers via attributes
2 parents 11c38d3 + 65168cb commit 5ba9880

File tree

14 files changed

+377
-132
lines changed

14 files changed

+377
-132
lines changed

clover.xml

Lines changed: 122 additions & 96 deletions
Large diffs are not rendered by default.

docs/Normalizers.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,21 @@ final readonly class GoalTodoNormalizer extends Normalizer
7777
}
7878
```
7979

80-
Next step is to add this new normalizer to the todo class:
80+
Next step is to add this new normalizer to the todo class, either using
81+
an attribute:
82+
83+
```php
84+
use Nuxtifyts\PhpDto\Data;
85+
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
86+
87+
#[WithNormalizer(GoalTodoNormalizer::class)]
88+
final readonly class TodoData extends Data
89+
{
90+
// ...
91+
}
92+
```
93+
94+
Or using a method:
8195

8296
```php
8397
use Nuxtifyts\PhpDto\Data;
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\Attributes\Class;
4+
5+
use Attribute;
6+
use InvalidArgumentException;
7+
use Nuxtifyts\PhpDto\Normalizers\Normalizer;
8+
use Nuxtifyts\PhpDto\Support\Arr;
9+
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
class WithNormalizer
12+
{
13+
/** @var array<array-key, class-string<Normalizer>> */
14+
public array $classStrings;
15+
16+
/**
17+
* @param class-string<Normalizer> $classString
18+
* @param class-string<Normalizer> ...$classStrings
19+
*/
20+
public function __construct(string $classString, string ...$classStrings)
21+
{
22+
$arrOfClassStrings = [$classString, ...$classStrings];
23+
24+
if (!Arr::isArrayOfClassStrings($arrOfClassStrings, Normalizer::class)) {
25+
throw new InvalidArgumentException('expects a list of class strings of normalizers');
26+
}
27+
28+
$this->classStrings = $arrOfClassStrings;
29+
}
30+
}

src/Concerns/BaseData.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ trait BaseData
2222
final public static function create(mixed ...$args): static
2323
{
2424
try {
25-
$value = static::normalizeValue($args, static::class)
26-
?: static::normalizeValue($args[0] ?? [], static::class);
25+
/** @var ClassContext<static> $context */
26+
$context = ClassContext::getInstance(static::class);
27+
28+
$value = static::normalizeValue($args, static::class, $context->normalizers)
29+
?: static::normalizeValue($args[0] ?? [], static::class, $context->normalizers);
2730

2831
if ($value === false) {
2932
throw DataCreationException::invalidParamsPassed(static::class);
3033
}
3134

32-
/** @var ClassContext<static> $context */
33-
$context = ClassContext::getInstance(new ReflectionClass(static::class));
34-
3535
$data = DeserializePipeline::createFromArray()
3636
->sendThenReturn(new DeserializePipelinePassable(
3737
classContext: $context,
@@ -51,15 +51,15 @@ classContext: $context,
5151
final public static function from(mixed $value): static
5252
{
5353
try {
54-
$value = static::normalizeValue($value, static::class);
54+
/** @var ClassContext<static> $context */
55+
$context = ClassContext::getInstance(static::class);
56+
57+
$value = static::normalizeValue($value, static::class, $context->normalizers);
5558

5659
if ($value === false) {
5760
throw DeserializeException::invalidValue();
5861
}
5962

60-
/** @var ClassContext<static> $context */
61-
$context = ClassContext::getInstance(new ReflectionClass(static::class));
62-
6363
$data = DeserializePipeline::hydrateFromArray()
6464
->sendThenReturn(new DeserializePipelinePassable(
6565
classContext: $context,
@@ -126,7 +126,7 @@ protected static function instanceWithConstructorCallFrom(ClassContext $context,
126126
final public function jsonSerialize(): array
127127
{
128128
try {
129-
$context = ClassContext::getInstance(new ReflectionClass($this));
129+
$context = ClassContext::getInstance($this::class);
130130

131131
$serializedData = [];
132132
foreach ($context->properties as $propertyContext) {

src/Concerns/CloneableData.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ public function with(mixed ...$args): static
2222
throw DataCreationException::invalidParamsPassed(static::class);
2323
}
2424

25-
$value = static::normalizeValue($args, static::class)
26-
?: static::normalizeValue($args[0], static::class);
25+
/** @var ClassContext<static> $context */
26+
$context = ClassContext::getInstance(static::class);
27+
28+
$value = static::normalizeValue($args, static::class, $context->normalizers)
29+
?: static::normalizeValue($args[0], static::class, $context->normalizers);
2730

2831
if ($value === false) {
2932
throw DataCreationException::invalidParamsPassed(static::class);
3033
}
3134

32-
/** @var ClassContext<static> $context */
33-
$context = ClassContext::getInstance(new ReflectionClass(static::class));
34-
3535
return $context->hasComputedProperties
3636
? $this->cloneInstanceWithConstructorCall($context, $value)
3737
: $this->cloneInstanceWithoutConstructorCall($context, $value);

src/Concerns/EmptyData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static function empty(): static
1616
{
1717
try {
1818
/** @var ClassContext<static> $classContext */
19-
$classContext = ClassContext::getInstance(new ReflectionClass(static::class));
19+
$classContext = ClassContext::getInstance(static::class);
2020

2121
return $classContext->emptyValue();
2222
} catch (Throwable $t) {

src/Contexts/ClassContext.php

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace Nuxtifyts\PhpDto\Contexts;
44

5+
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
56
use Nuxtifyts\PhpDto\Data;
67
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
78
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
9+
use Nuxtifyts\PhpDto\Normalizers\Normalizer;
10+
use ReflectionAttribute;
811
use ReflectionException;
912
use ReflectionParameter;
1013
use ReflectionClass;
@@ -30,6 +33,9 @@ class ClassContext
3033
/** @var list<string> List of param names */
3134
public readonly array $constructorParams;
3235

36+
/** @var array<array-key, class-string<Normalizer>> */
37+
private(set) array $normalizers = [];
38+
3339
/**
3440
* @param ReflectionClass<T> $reflection
3541
*
@@ -43,6 +49,7 @@ final private function __construct(
4349
static fn (ReflectionParameter $param) => $param->getName(),
4450
$this->reflection->getConstructor()?->getParameters() ?? [],
4551
);
52+
$this->syncClassAttributes();
4653
}
4754

4855
public bool $hasComputedProperties {
@@ -55,22 +62,33 @@ final private function __construct(
5562
}
5663

5764
/**
58-
* @param ReflectionClass<T> $reflectionClass
65+
* @param ReflectionClass<T>|class-string<T> $reflectionClass
5966
*
6067
* @throws UnsupportedTypeException
68+
* @throws ReflectionException
6169
*/
62-
final public static function getInstance(ReflectionClass $reflectionClass): static
70+
final public static function getInstance(string|ReflectionClass $reflectionClass): static
6371
{
72+
$instance = self::$_instances[self::getKey($reflectionClass)] ?? null;
73+
74+
if ($instance) {
75+
return $instance;
76+
}
77+
78+
if (is_string($reflectionClass)) {
79+
$reflectionClass = new ReflectionClass($reflectionClass);
80+
}
81+
6482
return self::$_instances[self::getKey($reflectionClass)]
65-
??= new static($reflectionClass);
83+
= new static($reflectionClass);
6684
}
6785

6886
/**
69-
* @param ReflectionClass<T> $reflectionClass
87+
* @param ReflectionClass<T>|class-string<T> $reflectionClass
7088
*/
71-
private static function getKey(ReflectionClass $reflectionClass): string
89+
private static function getKey(string|ReflectionClass $reflectionClass): string
7290
{
73-
return $reflectionClass->getName();
91+
return is_string($reflectionClass) ? $reflectionClass : $reflectionClass->getName();
7492
}
7593

7694
/**
@@ -91,6 +109,17 @@ private static function getPropertyContexts(ReflectionClass $reflectionClass): a
91109
return $properties;
92110
}
93111

112+
private function syncClassAttributes(): void
113+
{
114+
foreach ($this->reflection->getAttributes(WithNormalizer::class) as $withNormalizerAttribute) {
115+
/** @var ReflectionAttribute<WithNormalizer> $withNormalizerAttribute */
116+
$this->normalizers = array_values([
117+
...$this->normalizers,
118+
...$withNormalizerAttribute->newInstance()->classStrings
119+
]);
120+
}
121+
}
122+
94123
/**
95124
* @throws ReflectionException
96125
*

src/Normalizers/Concerns/HasNormalizers.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@
33
namespace Nuxtifyts\PhpDto\Normalizers\Concerns;
44

55
use Nuxtifyts\PhpDto\Configuration\DataConfiguration;
6+
use Nuxtifyts\PhpDto\Contexts\ClassContext;
67
use Nuxtifyts\PhpDto\Data;
78
use Nuxtifyts\PhpDto\Exceptions\DataConfigurationException;
89
use Nuxtifyts\PhpDto\Normalizers\Normalizer;
10+
use ReflectionClass;
911

1012
trait HasNormalizers
1113
{
1214
/**
1315
* @param class-string<Data> $class
16+
* @param array<array-key, class-string<Normalizer>> $classNormalizers
1417
*
1518
* @return array<string, mixed>|false
1619
*
1720
* @throws DataConfigurationException
1821
*/
19-
protected static function normalizeValue(mixed $value, string $class): array|false
20-
{
21-
foreach (static::allNormalizer() as $normalizer) {
22+
protected static function normalizeValue(
23+
mixed $value,
24+
string $class,
25+
array $classNormalizers = []
26+
): array|false {
27+
foreach (static::allNormalizer($classNormalizers) as $normalizer) {
2228
$normalized = new $normalizer($value, $class)->normalize();
2329

2430
if ($normalized !== false) {
@@ -30,13 +36,16 @@ protected static function normalizeValue(mixed $value, string $class): array|fal
3036
}
3137

3238
/**
39+
* @param array<array-key, class-string<Normalizer>> $classNormalizers
40+
*
3341
* @return list<class-string<Normalizer>>
3442
*
3543
* @throws DataConfigurationException
3644
*/
37-
final protected static function allNormalizer(): array
45+
final protected static function allNormalizer(array $classNormalizers = []): array
3846
{
3947
return array_values(array_unique([
48+
...$classNormalizers,
4049
...static::normalizers(),
4150
...DataConfiguration::getInstance()->normalizers->baseNormalizers,
4251
]));
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Dummies;
4+
5+
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
6+
use Nuxtifyts\PhpDto\Data;
7+
use Nuxtifyts\PhpDto\Tests\Dummies\Normalizers\DummyNormalizer;
8+
9+
#[WithNormalizer(DummyNormalizer::class)]
10+
final readonly class DummyWithNormalizerData extends Data
11+
{
12+
public function __construct() {
13+
}
14+
}

tests/Dummies/NonData/Human.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Dummies\NonData;
4+
5+
class Human
6+
{
7+
public function __construct(
8+
public string $name,
9+
public string $surname
10+
) {
11+
}
12+
}

0 commit comments

Comments
 (0)