Skip to content

Add support for normalizers via attributes #13

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
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
218 changes: 122 additions & 96 deletions clover.xml

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion docs/Normalizers.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,21 @@ final readonly class GoalTodoNormalizer extends Normalizer
}
```

Next step is to add this new normalizer to the todo class:
Next step is to add this new normalizer to the todo class, either using
an attribute:

```php
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;

#[WithNormalizer(GoalTodoNormalizer::class)]
final readonly class TodoData extends Data
{
// ...
}
```

Or using a method:

```php
use Nuxtifyts\PhpDto\Data;
Expand Down
30 changes: 30 additions & 0 deletions src/Attributes/Class/WithNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Nuxtifyts\PhpDto\Attributes\Class;

use Attribute;
use InvalidArgumentException;
use Nuxtifyts\PhpDto\Normalizers\Normalizer;
use Nuxtifyts\PhpDto\Support\Arr;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class WithNormalizer
{
/** @var array<array-key, class-string<Normalizer>> */
public array $classStrings;

/**
* @param class-string<Normalizer> $classString
* @param class-string<Normalizer> ...$classStrings
*/
public function __construct(string $classString, string ...$classStrings)
{
$arrOfClassStrings = [$classString, ...$classStrings];

if (!Arr::isArrayOfClassStrings($arrOfClassStrings, Normalizer::class)) {
throw new InvalidArgumentException('expects a list of class strings of normalizers');
}

$this->classStrings = $arrOfClassStrings;
}
}
20 changes: 10 additions & 10 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ trait BaseData
final public static function create(mixed ...$args): static
{
try {
$value = static::normalizeValue($args, static::class)
?: static::normalizeValue($args[0] ?? [], static::class);
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(static::class);

$value = static::normalizeValue($args, static::class, $context->normalizers)
?: static::normalizeValue($args[0] ?? [], static::class, $context->normalizers);

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

/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(new ReflectionClass(static::class));

$data = DeserializePipeline::createFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
Expand All @@ -51,15 +51,15 @@ classContext: $context,
final public static function from(mixed $value): static
{
try {
$value = static::normalizeValue($value, static::class);
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(static::class);

$value = static::normalizeValue($value, static::class, $context->normalizers);

if ($value === false) {
throw DeserializeException::invalidValue();
}

/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(new ReflectionClass(static::class));

$data = DeserializePipeline::hydrateFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
Expand Down Expand Up @@ -126,7 +126,7 @@ protected static function instanceWithConstructorCallFrom(ClassContext $context,
final public function jsonSerialize(): array
{
try {
$context = ClassContext::getInstance(new ReflectionClass($this));
$context = ClassContext::getInstance($this::class);

$serializedData = [];
foreach ($context->properties as $propertyContext) {
Expand Down
10 changes: 5 additions & 5 deletions src/Concerns/CloneableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ public function with(mixed ...$args): static
throw DataCreationException::invalidParamsPassed(static::class);
}

$value = static::normalizeValue($args, static::class)
?: static::normalizeValue($args[0], static::class);
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(static::class);

$value = static::normalizeValue($args, static::class, $context->normalizers)
?: static::normalizeValue($args[0], static::class, $context->normalizers);

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

/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(new ReflectionClass(static::class));

return $context->hasComputedProperties
? $this->cloneInstanceWithConstructorCall($context, $value)
: $this->cloneInstanceWithoutConstructorCall($context, $value);
Expand Down
2 changes: 1 addition & 1 deletion src/Concerns/EmptyData.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static function empty(): static
{
try {
/** @var ClassContext<static> $classContext */
$classContext = ClassContext::getInstance(new ReflectionClass(static::class));
$classContext = ClassContext::getInstance(static::class);

return $classContext->emptyValue();
} catch (Throwable $t) {
Expand Down
41 changes: 35 additions & 6 deletions src/Contexts/ClassContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace Nuxtifyts\PhpDto\Contexts;

use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
use Nuxtifyts\PhpDto\Normalizers\Normalizer;
use ReflectionAttribute;
use ReflectionException;
use ReflectionParameter;
use ReflectionClass;
Expand All @@ -30,6 +33,9 @@ class ClassContext
/** @var list<string> List of param names */
public readonly array $constructorParams;

/** @var array<array-key, class-string<Normalizer>> */
private(set) array $normalizers = [];

/**
* @param ReflectionClass<T> $reflection
*
Expand All @@ -43,6 +49,7 @@ final private function __construct(
static fn (ReflectionParameter $param) => $param->getName(),
$this->reflection->getConstructor()?->getParameters() ?? [],
);
$this->syncClassAttributes();
}

public bool $hasComputedProperties {
Expand All @@ -55,22 +62,33 @@ final private function __construct(
}

/**
* @param ReflectionClass<T> $reflectionClass
* @param ReflectionClass<T>|class-string<T> $reflectionClass
*
* @throws UnsupportedTypeException
* @throws ReflectionException
*/
final public static function getInstance(ReflectionClass $reflectionClass): static
final public static function getInstance(string|ReflectionClass $reflectionClass): static
{
$instance = self::$_instances[self::getKey($reflectionClass)] ?? null;

if ($instance) {
return $instance;
}

if (is_string($reflectionClass)) {
$reflectionClass = new ReflectionClass($reflectionClass);
}

return self::$_instances[self::getKey($reflectionClass)]
??= new static($reflectionClass);
= new static($reflectionClass);
}

/**
* @param ReflectionClass<T> $reflectionClass
* @param ReflectionClass<T>|class-string<T> $reflectionClass
*/
private static function getKey(ReflectionClass $reflectionClass): string
private static function getKey(string|ReflectionClass $reflectionClass): string
{
return $reflectionClass->getName();
return is_string($reflectionClass) ? $reflectionClass : $reflectionClass->getName();
}

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

private function syncClassAttributes(): void
{
foreach ($this->reflection->getAttributes(WithNormalizer::class) as $withNormalizerAttribute) {
/** @var ReflectionAttribute<WithNormalizer> $withNormalizerAttribute */
$this->normalizers = array_values([
...$this->normalizers,
...$withNormalizerAttribute->newInstance()->classStrings
]);
}
}

/**
* @throws ReflectionException
*
Expand Down
17 changes: 13 additions & 4 deletions src/Normalizers/Concerns/HasNormalizers.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@
namespace Nuxtifyts\PhpDto\Normalizers\Concerns;

use Nuxtifyts\PhpDto\Configuration\DataConfiguration;
use Nuxtifyts\PhpDto\Contexts\ClassContext;
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Exceptions\DataConfigurationException;
use Nuxtifyts\PhpDto\Normalizers\Normalizer;
use ReflectionClass;

trait HasNormalizers
{
/**
* @param class-string<Data> $class
* @param array<array-key, class-string<Normalizer>> $classNormalizers
*
* @return array<string, mixed>|false
*
* @throws DataConfigurationException
*/
protected static function normalizeValue(mixed $value, string $class): array|false
{
foreach (static::allNormalizer() as $normalizer) {
protected static function normalizeValue(
mixed $value,
string $class,
array $classNormalizers = []
): array|false {
foreach (static::allNormalizer($classNormalizers) as $normalizer) {
$normalized = new $normalizer($value, $class)->normalize();

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

/**
* @param array<array-key, class-string<Normalizer>> $classNormalizers
*
* @return list<class-string<Normalizer>>
*
* @throws DataConfigurationException
*/
final protected static function allNormalizer(): array
final protected static function allNormalizer(array $classNormalizers = []): array
{
return array_values(array_unique([
...$classNormalizers,
...static::normalizers(),
...DataConfiguration::getInstance()->normalizers->baseNormalizers,
]));
Expand Down
14 changes: 14 additions & 0 deletions tests/Dummies/DummyWithNormalizerData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Nuxtifyts\PhpDto\Tests\Dummies;

use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Tests\Dummies\Normalizers\DummyNormalizer;

#[WithNormalizer(DummyNormalizer::class)]
final readonly class DummyWithNormalizerData extends Data
{
public function __construct() {
}
}
12 changes: 12 additions & 0 deletions tests/Dummies/NonData/Human.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Nuxtifyts\PhpDto\Tests\Dummies\NonData;

class Human
{
public function __construct(
public string $name,
public string $surname
) {
}
}
22 changes: 22 additions & 0 deletions tests/Dummies/Normalizers/HumanToPersonNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Nuxtifyts\PhpDto\Tests\Dummies\Normalizers;

use Nuxtifyts\PhpDto\Tests\Dummies\NonData\Human;
use Nuxtifyts\PhpDto\Normalizers\Normalizer;

final readonly class HumanToPersonNormalizer extends Normalizer
{
/**
* @return array<string, mixed>|false
*/
public function normalize(): array|false
{
return $this->value instanceof Human
? [
'firstName' => $this->value->name,
'lastName' => $this->value->surname
]
: false;
}
}
5 changes: 5 additions & 0 deletions tests/Dummies/PersonData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

namespace Nuxtifyts\PhpDto\Tests\Dummies;

use Nuxtifyts\PhpDto\Tests\Dummies\Normalizers\DummyNormalizer;
use Nuxtifyts\PhpDto\Tests\Dummies\Normalizers\HumanToPersonNormalizer;
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
use Nuxtifyts\PhpDto\Attributes\Property\Aliases;
use Nuxtifyts\PhpDto\Attributes\Property\Computed;
use Nuxtifyts\PhpDto\Data;

#[WithNormalizer(DummyNormalizer::class)]
#[WithNormalizer(HumanToPersonNormalizer::class)]
final readonly class PersonData extends Data
{
#[Computed]
Expand Down
Loading
Loading