Skip to content
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
18 changes: 6 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,16 @@ parameters:
path: src/Metadata/ClassMetadata.php

-
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
identifier: assign.propertyType
count: 1
path: src/Normalizer/EnumNormalizer.php

-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Hydrator\:\:hydrate\(\) expects array\<string, mixed\>, array given\.$#'
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Middleware\\Middleware\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
identifier: argument.type
count: 1
path: src/Normalizer/ObjectMapNormalizer.php
count: 3
path: src/MetadataHydrator.php

-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Hydrator\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
identifier: argument.type
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
identifier: assign.propertyType
count: 1
path: src/Normalizer/ObjectNormalizer.php
path: src/Normalizer/EnumNormalizer.php

-
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\ObjectNormalizer\:\:\$className \(class\-string\|null\) does not accept string\.$#'
Expand Down
21 changes: 21 additions & 0 deletions src/ArrayDataRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use RuntimeException;

use function sprintf;

final class ArrayDataRequired extends RuntimeException implements HydratorException
{
/** @param class-string $class */
public function __construct(string $class)
{
parent::__construct(sprintf(
'The data for the class "%s" must be an array. If you want to use another data type, you need to add a normalizer to the class.',
$class,
));
}
}
11 changes: 3 additions & 8 deletions src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ interface Hydrator
{
/**
* @param class-string<T> $class
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
Expand All @@ -17,12 +16,8 @@ interface Hydrator
*
* @template T of object
*/
public function hydrate(string $class, array $data, array $context = []): object;
public function hydrate(string $class, mixed $data, array $context = []): object;

/**
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*/
public function extract(object $object, array $context = []): array;
/** @param array<string, mixed> $context */
public function extract(object $object, array $context = []): mixed;
}
20 changes: 20 additions & 0 deletions src/Metadata/AttributeMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
use Patchlevel\Hydrator\Normalizer\ArrayShapeNormalizer;
use Patchlevel\Hydrator\Normalizer\Normalizer;
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;
use Patchlevel\Hydrator\Normalizer\TypeAwareNormalizer;
use ReflectionAttribute;
use ReflectionClass;
Expand Down Expand Up @@ -73,6 +74,7 @@
{
$metadata = new ClassMetadata(
$reflectionClass,
$this->getNormalizerOnClass($reflectionClass),
$this->getPropertyMetadataList($reflectionClass),
$this->getLazy($reflectionClass),
);
Expand Down Expand Up @@ -189,6 +191,7 @@

return new ClassMetadata(
$parent->reflection,
$child->normalizer ?? $parent->normalizer,

Check warning on line 194 in src/Metadata/AttributeMetadataFactory.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "Coalesce": @@ @@ return new ClassMetadata( $parent->reflection, - $child->normalizer ?? $parent->normalizer, + $parent->normalizer ?? $child->normalizer, array_values($properties), $child->lazy ?? $parent->lazy, array_merge($parent->extras, $child->extras),
array_values($properties),
$child->lazy ?? $parent->lazy,
array_merge($parent->extras, $child->extras),
Expand All @@ -212,6 +215,23 @@
return $normalizer;
}

/** @param ReflectionClass<object> $reflectionClass */
private function getNormalizerOnClass(ReflectionClass $reflectionClass): Normalizer|null
{
$type = Type::object($reflectionClass->getName());
$normalizer = $this->inferNormalizerByType($type);

if ($normalizer instanceof ObjectNormalizer) {
return null;

Check warning on line 225 in src/Metadata/AttributeMetadataFactory.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ReturnRemoval": @@ @@ $normalizer = $this->inferNormalizerByType($type); if ($normalizer instanceof ObjectNormalizer) { - return null; + } if ($normalizer instanceof TypeAwareNormalizer) {
}

if ($normalizer instanceof TypeAwareNormalizer) {
$normalizer->handleType($type);

Check warning on line 229 in src/Metadata/AttributeMetadataFactory.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ } if ($normalizer instanceof TypeAwareNormalizer) { - $normalizer->handleType($type); + } return $normalizer;
}

return $normalizer;
}

private function findNormalizerOnProperty(ReflectionProperty $reflectionProperty): Normalizer|null
{
/** @var list<ReflectionAttribute<Normalizer>> $attributeReflectionList */
Expand Down
5 changes: 5 additions & 0 deletions src/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

namespace Patchlevel\Hydrator\Metadata;

use Patchlevel\Hydrator\Normalizer\Normalizer;
use ReflectionClass;

/**
* @phpstan-type serialized array{
* className: class-string,
* normalizer: Normalizer|null,
* properties: array<string, PropertyMetadata>,
* lazy: bool|null,
* extras: array<string, mixed>,
Expand All @@ -30,6 +32,7 @@ final class ClassMetadata
*/
public function __construct(
public readonly ReflectionClass $reflection,
public Normalizer|null $normalizer = null,
array $properties = [],
public bool|null $lazy = null,
public array $extras = [],
Expand Down Expand Up @@ -67,6 +70,7 @@ public function __serialize(): array
{
return [
'className' => $this->className,
'normalizer' => $this->normalizer,
'properties' => $this->properties,
'lazy' => $this->lazy,
'extras' => $this->extras,
Expand All @@ -77,6 +81,7 @@ public function __serialize(): array
public function __unserialize(array $data): void
{
$this->reflection = new ReflectionClass($data['className']);
$this->normalizer = $data['normalizer'];
$this->properties = $data['properties'];
$this->lazy = $data['lazy'];
$this->extras = $data['extras'];
Expand Down
31 changes: 23 additions & 8 deletions src/MetadataHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use ReflectionClass;

use function array_key_exists;
use function is_array;

use const PHP_VERSION_ID;

Expand All @@ -33,21 +34,34 @@ public function __construct(

/**
* @param class-string<T> $class
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @template T of object
*/
public function hydrate(string $class, array $data, array $context = []): object
public function hydrate(string $class, mixed $data, array $context = []): object
{
try {
$metadata = $this->metadata($class);
} catch (ClassNotFound $e) {
throw new ClassNotSupported($class, $e);
}

if ($metadata->normalizer) {
$return = $metadata->normalizer->denormalize($data, $context);

if (!$return instanceof $class) {
throw new ObjectRequired($class, $metadata->normalizer::class);
}

return $return;
}

if (!is_array($data)) {
throw new ArrayDataRequired($class);
}

if (PHP_VERSION_ID < 80400) {
$stack = new Stack($this->middlewares);

Expand All @@ -71,14 +85,15 @@ function () use ($metadata, $data, $context): object {
);
}

/**
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*/
public function extract(object $object, array $context = []): array
/** @param array<string, mixed> $context */
public function extract(object $object, array $context = []): mixed
{
$metadata = $this->metadata($object::class);

if ($metadata->normalizer) {
return $metadata->normalizer->normalize($object, $context);
}

$stack = new Stack($this->middlewares);

return $stack->next()->extract($metadata, $object, $context, $stack);
Expand Down
5 changes: 5 additions & 0 deletions src/Normalizer/ObjectMapNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ public function normalize(mixed $value, array $context): mixed
}

$data = $this->hydrator->extract($value);

if (!is_array($data)) {
throw InvalidArgument::withWrongType('array<string, mixed>', $data);
}

$data[$this->typeFieldName] = $this->classToTypeMap[$value::class];

return $data;
Expand Down
14 changes: 2 additions & 12 deletions src/Normalizer/ObjectNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\TemplateType;

use function is_array;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
final class ObjectNormalizer implements Normalizer, TypeAwareNormalizer, HydratorAwareNormalizer
{
Expand All @@ -25,12 +23,8 @@ public function __construct(
) {
}

/**
* @param array<string, mixed> $context
*
* @return array<string, mixed>|null
*/
public function normalize(mixed $value, array $context): array|null
/** @param array<string, mixed> $context */
public function normalize(mixed $value, array $context): mixed
{
if (!$this->hydrator) {
throw new MissingHydrator();
Expand Down Expand Up @@ -60,10 +54,6 @@ public function denormalize(mixed $value, array $context): object|null
return null;
}

if (!is_array($value)) {
throw InvalidArgument::withWrongType('array<string, mixed>|null', $value);
}

$className = $this->getClassName();

return $this->hydrator->hydrate($className, $value, $context);
Expand Down
31 changes: 31 additions & 0 deletions src/ObjectRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Normalizer\Normalizer;
use RuntimeException;

use function sprintf;

final class ObjectRequired extends RuntimeException implements HydratorException
{
/**
* @param class-string $class
* @param class-string<Normalizer> $normalizerClass
*/
public function __construct(
string $class,
string $normalizerClass,
) {
parent::__construct(
sprintf(
'The result of the normalizer "%s" for the class "%s" must be an instance of "%s".',
$normalizerClass,
$class,
$class,
),
);
}
}
1 change: 1 addition & 0 deletions tests/Unit/Extension/Lifecycle/LifecycleExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function testIntegration(): void

$extractedData = $hydrator->extract($object);

self::assertIsArray($extractedData);
self::assertSame('foo [preHydrate] [postHydrate] [preExtract] [postExtract]', $extractedData['name']);
}
}
8 changes: 8 additions & 0 deletions tests/Unit/Metadata/AttributeMetadataFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,12 @@ class {

self::assertTrue($metadata->lazy);
}

public function testClassMetadataWithNormalizer(): void
{
$metadataFactory = new AttributeMetadataFactory();
$metadata = $metadataFactory->metadata(ProfileId::class);

self::assertInstanceOf(IdNormalizer::class, $metadata->normalizer);
}
}
1 change: 1 addition & 0 deletions tests/Unit/Metadata/ClassMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function testPropertiesHashmap(): void

$classMetadata = new ClassMetadata(
$reflection,
null,
[$fooMetadata, $barMetadata],
);

Expand Down
31 changes: 31 additions & 0 deletions tests/Unit/MetadataHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Patchlevel\Hydrator\ArrayDataRequired;
use Patchlevel\Hydrator\CircularReference;
use Patchlevel\Hydrator\ClassNotSupported;
use Patchlevel\Hydrator\CoreExtension;
Expand Down Expand Up @@ -190,6 +191,15 @@ public function testExtractWithInlineNormalizer(): void
);
}

public function testExtractWithClassNormalizer(): void
{
$data = $this->hydrator->extract(
ProfileId::fromString('id'),
);

self::assertEquals('id', $data);
}

public function testHydrate(): void
{
$expected = new ProfileCreated(
Expand All @@ -216,6 +226,17 @@ public function testHydrateUnknownClass(): void
);
}

public function testHydrateWithArrayDataRequired(): void
{
$this->expectException(ArrayDataRequired::class);
$this->expectExceptionMessage('The data for the class "Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated" must be an array. If you want to use another data type, you need to add a normalizer to the class.');

$this->hydrator->hydrate(
ProfileCreated::class,
'foo',
);
}

public function testHydrateWithDefaults(): void
{
$object = $this->hydrator->hydrate(
Expand Down Expand Up @@ -471,6 +492,16 @@ public function testHydrateWithInferNormalizerWitIterables(): void
self::assertEquals($expected, $event);
}

public function testHydrateWithClassNormalizer(): void
{
$object = $this->hydrator->hydrate(
ProfileId::class,
'id',
);

self::assertEquals(ProfileId::fromString('id'), $object);
}

#[RequiresPhp('>=8.4')]
public function testLazyHydrate(): void
{
Expand Down
Loading
Loading