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
12 changes: 6 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,6 @@ parameters:
count: 3
path: src/Metadata/AttributeMetadataFactory.php

-
message: '#^Property Patchlevel\\Hydrator\\Metadata\\ClassMetadata\<T of object \= object\>\:\:\$reflection \(ReflectionClass\<T of object \= object\>\) does not accept ReflectionClass\<object\>\.$#'
identifier: assign.propertyType
count: 1
path: src/Metadata/ClassMetadata.php

-
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
identifier: assign.propertyType
Expand Down Expand Up @@ -185,3 +179,9 @@ parameters:
identifier: cast.string
count: 2
path: tests/Unit/Normalizer/ArrayShapeNormalizerTest.php

-
message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\StackHydrator\:\:hydrate\(\) expects class\-string\<Unknown\>, string given\.$#'
identifier: argument.type
count: 1
path: tests/Unit/StackHydratorTest.php
17 changes: 17 additions & 0 deletions src/CoreExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Middleware\TransformMiddleware;

final class CoreExtension implements Extension
{
public function configure(StackHydratorBuilder $builder): void
{
$builder->addMiddleware(new TransformMiddleware(), -64);
$builder->addGuesser(new BuiltInGuesser(), -64);
}
}
10 changes: 10 additions & 0 deletions src/Extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

interface Extension
{
public function configure(StackHydratorBuilder $builder): void;
}
76 changes: 64 additions & 12 deletions src/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,58 @@
namespace Patchlevel\Hydrator\Metadata;

use ReflectionClass;
use ReflectionParameter;

use function array_values;

/**
* @psalm-type serialized array{
* className: class-string,
* properties: list<PropertyMetadata>,
* className: class-string<T>,
* properties: array<string, PropertyMetadata>,
* dataSubjectIdField: string|null,
* postHydrateCallbacks: list<CallbackMetadata>,
* preExtractCallbacks: list<CallbackMetadata>,
* lazy: bool|null,
* extras: array<string, mixed>
* }
* @template T of object = object
*/
final readonly class ClassMetadata
final class ClassMetadata
{
/** @var class-string<T> */
public readonly string $className;

/** @var array<string, PropertyMetadata> */
public readonly array $properties;

/** @var array<string, ReflectionParameter>|null */
private array|null $promotedConstructorDefaults = null;

/**
* @param ReflectionClass<T> $reflection
* @param list<PropertyMetadata> $properties
* @param list<CallbackMetadata> $postHydrateCallbacks
* @param list<CallbackMetadata> $preExtractCallbacks
* @param array<string, mixed> $extras
*/
public function __construct(
private ReflectionClass $reflection,
private array $properties = [],
private string|null $dataSubjectIdField = null,
private array $postHydrateCallbacks = [],
private array $preExtractCallbacks = [],
private bool|null $lazy = null,
public readonly ReflectionClass $reflection,
array $properties = [],
public string|null $dataSubjectIdField = null,
public array $postHydrateCallbacks = [],
public array $preExtractCallbacks = [],
public bool|null $lazy = null,
public array $extras = [],
) {
$this->className = $reflection->getName();

$map = [];

foreach ($properties as $property) {
$map[$property->propertyName] = $property;
}

$this->properties = $map;
}

/** @return ReflectionClass<T> */
Expand All @@ -44,13 +68,13 @@ public function reflection(): ReflectionClass
/** @return class-string<T> */
public function className(): string
{
return $this->reflection->getName();
return $this->className;
}

/** @return list<PropertyMetadata> */
public function properties(): array
{
return $this->properties;
return array_values($this->properties);
}

/** @return list<CallbackMetadata> */
Expand Down Expand Up @@ -92,16 +116,43 @@ public function newInstance(): object
return $this->reflection->newInstanceWithoutConstructor();
}

/** @return array<string, ReflectionParameter> */
public function promotedConstructorDefaults(): array
{
if ($this->promotedConstructorDefaults !== null) {
return $this->promotedConstructorDefaults;
}

$constructor = $this->reflection->getConstructor();

if (!$constructor) {
return $this->promotedConstructorDefaults = [];
}

$result = [];

foreach ($constructor->getParameters() as $parameter) {
if (!$parameter->isPromoted() || !$parameter->isDefaultValueAvailable()) {
continue;
}

$result[$parameter->getName()] = $parameter;
}

return $this->promotedConstructorDefaults = $result;
}

/** @return serialized */
public function __serialize(): array
{
return [
'className' => $this->reflection->getName(),
'className' => $this->className,
'properties' => $this->properties,
'dataSubjectIdField' => $this->dataSubjectIdField,
'postHydrateCallbacks' => $this->postHydrateCallbacks,
'preExtractCallbacks' => $this->preExtractCallbacks,
'lazy' => $this->lazy,
'extras' => $this->extras,
];
}

Expand All @@ -114,5 +165,6 @@ public function __unserialize(array $data): void
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
$this->preExtractCallbacks = $data['preExtractCallbacks'];
$this->lazy = $data['lazy'];
$this->extras = $data['extras'];
}
}
35 changes: 35 additions & 0 deletions src/Metadata/EnrichingMetadataFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Metadata;

final readonly class EnrichingMetadataFactory implements MetadataFactory
{
/** @param iterable<MetadataEnricher> $enrichers */
public function __construct(
private MetadataFactory $factory,
private iterable $enrichers,
) {
}

/**
* @param class-string<T> $class
*
* @return ClassMetadata<T>
*
* @throws ClassNotFound if the class does not exist.
*
* @template T of object
*/
public function metadata(string $class): ClassMetadata
{
$metadata = $this->factory->metadata($class);

foreach ($this->enrichers as $enricher) {
$enricher->enrich($metadata);
}

return $metadata;
}
}
10 changes: 10 additions & 0 deletions src/Metadata/MetadataEnricher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Metadata;

interface MetadataEnricher
{
public function enrich(ClassMetadata $classMetadata): void;
}
31 changes: 21 additions & 10 deletions src/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,31 @@
* fieldName: string,
* normalizer: Normalizer|null,
* isPersonalData: bool,
* personalDataFallback: mixed
* personalDataFallback: mixed,
* extras: array<string, mixed>
* }
*/
final class PropertyMetadata
{
private const ENCRYPTED_PREFIX = '!';

/** @param (callable(string, mixed):mixed)|null $personalDataFallbackCallable */
public readonly string $propertyName;

/**
* @param (callable(string, mixed):mixed)|null $personalDataFallbackCallable
* @param array<string, mixed> $extras
*/
public function __construct(
private readonly ReflectionProperty $reflection,
private readonly string $fieldName,
private readonly Normalizer|null $normalizer = null,
private readonly bool $isPersonalData = false,
private readonly mixed $personalDataFallback = null,
private readonly mixed $personalDataFallbackCallable = null,
public readonly ReflectionProperty $reflection,
public string $fieldName,
public Normalizer|null $normalizer = null,
public readonly bool $isPersonalData = false,

Check warning on line 39 in src/Metadata/PropertyMetadata.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "FalseValue": @@ @@ public readonly ReflectionProperty $reflection, public string $fieldName, public Normalizer|null $normalizer = null, - public readonly bool $isPersonalData = false, + public readonly bool $isPersonalData = true, public readonly mixed $personalDataFallback = null, public readonly mixed $personalDataFallbackCallable = null, public array $extras = [],
public readonly mixed $personalDataFallback = null,
public readonly mixed $personalDataFallbackCallable = null,
public array $extras = [],
) {
$this->propertyName = $reflection->getName();

if (str_starts_with($fieldName, self::ENCRYPTED_PREFIX)) {
throw new InvalidArgumentException('fieldName must not start with !');
}
Expand All @@ -46,7 +55,7 @@

public function propertyName(): string
{
return $this->reflection->getName();
return $this->propertyName;
}

public function fieldName(): string
Expand Down Expand Up @@ -99,11 +108,12 @@
{
return [
'className' => $this->reflection->getDeclaringClass()->getName(),
'property' => $this->reflection->getName(),
'property' => $this->propertyName,
'fieldName' => $this->fieldName,
'normalizer' => $this->normalizer,
'isPersonalData' => $this->isPersonalData,
'personalDataFallback' => $this->personalDataFallback,
'extras' => $this->extras,
];
}

Expand All @@ -115,5 +125,6 @@
$this->normalizer = $data['normalizer'];
$this->isPersonalData = $data['isPersonalData'];
$this->personalDataFallback = $data['personalDataFallback'];
$this->extras = $data['extras'];
}
}
32 changes: 32 additions & 0 deletions src/Middleware/Middleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Middleware;

use Patchlevel\Hydrator\Metadata\ClassMetadata;

interface Middleware
{
/**
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @template T of object
*/
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object;

/**
* @param ClassMetadata<T> $metadata
* @param T $object
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*
* @template T of object
*/
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array;
}
16 changes: 16 additions & 0 deletions src/Middleware/NoMoreMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Middleware;

use Patchlevel\Hydrator\HydratorException;
use RuntimeException;

final class NoMoreMiddleware extends RuntimeException implements HydratorException
{
public function __construct()
{
parent::__construct('no more middlewares');
}
}
29 changes: 29 additions & 0 deletions src/Middleware/Stack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Middleware;

final class Stack
{
private int $index = 0;

/** @param list<Middleware> $middlewares */
public function __construct(
private readonly array $middlewares,
) {
}

public function next(): Middleware
{
$next = $this->middlewares[$this->index] ?? null;

if ($next === null) {
throw new NoMoreMiddleware();
}

$this->index++;

return $next;
}
}
Loading
Loading