From d8710f96f8f63e71e47fa3c5674126326ea26ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 18:46:43 +0200 Subject: [PATCH 01/16] Mapping via discriminator --- src/Compiler/Mapper/Object/Discriminator.php | 21 ++ .../Mapper/Object/MapDiscriminatedObject.php | 149 +++++++++ .../DefaultMapperCompilerFactory.php | 55 +++- .../Object/Data/HierarchicalChildOneInput.php | 24 ++ .../Object/Data/HierarchicalChildTwoInput.php | 24 ++ .../Object/Data/HierarchicalParentInput.php | 30 ++ .../Data/HierarchicalParentInputMapper.php | 283 ++++++++++++++++++ .../Data/HierarchicalWithEnumChildInput.php | 16 + .../Data/HierarchicalWithEnumParentInput.php | 24 ++ .../HierarchicalWithEnumParentInputMapper.php | 129 ++++++++ .../Object/Data/HierarchicalWithEnumType.php | 10 + .../Mapper/Object/Data/ParentMapper.php | 283 ++++++++++++++++++ .../Object/MapDiscriminatedObjectTest.php | 174 +++++++++++ .../MapperFactory/Data/AnimalCatInput.php | 16 + .../MapperFactory/Data/AnimalDogInput.php | 17 ++ .../MapperFactory/Data/AnimalInput.php | 24 ++ .../MapperFactory/Data/AnimalType.php | 13 + .../DefaultMapperCompilerFactoryTest.php | 26 ++ 18 files changed, 1306 insertions(+), 12 deletions(-) create mode 100644 src/Compiler/Mapper/Object/Discriminator.php create mode 100644 src/Compiler/Mapper/Object/MapDiscriminatedObject.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalChildOneInput.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalChildTwoInput.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalParentInput.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumChildInput.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumType.php create mode 100644 tests/Compiler/Mapper/Object/Data/ParentMapper.php create mode 100644 tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php create mode 100644 tests/Compiler/MapperFactory/Data/AnimalCatInput.php create mode 100644 tests/Compiler/MapperFactory/Data/AnimalDogInput.php create mode 100644 tests/Compiler/MapperFactory/Data/AnimalInput.php create mode 100644 tests/Compiler/MapperFactory/Data/AnimalType.php diff --git a/src/Compiler/Mapper/Object/Discriminator.php b/src/Compiler/Mapper/Object/Discriminator.php new file mode 100644 index 0000000..d18e2e8 --- /dev/null +++ b/src/Compiler/Mapper/Object/Discriminator.php @@ -0,0 +1,21 @@ + + */ + public readonly array $mapping + ) + { + } + +} diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php new file mode 100644 index 0000000..a950d37 --- /dev/null +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -0,0 +1,149 @@ + $className + * @param array $objectMappers + * @param list $genericParameters + */ + public function __construct( + public readonly string $className, + public readonly MapString $discriminatorMapper, + public readonly string $discriminatorFieldName, + public readonly array $objectMappers, + public readonly array $genericParameters = [], + ) + { + } + + public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr + { + $objectMapperMethods = []; + + foreach ($this->objectMappers as $key => $objectMapper) { + $objectMapperMethodName = $builder->uniqMethodName('map' . ucfirst($key)); + $objectMapperMethod = $builder->mapperMethod($objectMapperMethodName, $objectMapper)->makePrivate()->getNode(); + $objectMapperMethods[$key] = $objectMapperMethodName; + + $builder->addMethod($objectMapperMethod); + } + + $validMappingKeysConstName = $builder->uniqConstantName('VALID_MAPPINGS', $objectMapperMethods); + $builder->addConstant($validMappingKeysConstName, $objectMapperMethods); + + $statements = [ + $builder->if($builder->not($builder->funcCall($builder->importFunction('is_array'), [$value])), [ + $builder->throw( + $builder->staticCall( + $builder->importClass(MappingFailedException::class), + 'incorrectType', + [$value, $path, $builder->val('array')], + ), + ), + ]), + ]; + + $isDiscriminatorPresent = $builder->funcCall($builder->importFunction('array_key_exists'), [$builder->val($this->discriminatorFieldName), $value]); + $isDiscriminatorMissing = $builder->not($isDiscriminatorPresent); + + $statements[] = $builder->if($isDiscriminatorMissing, [ + $builder->throw( + $builder->staticCall( + $builder->importClass(MappingFailedException::class), + 'missingKey', + [$path, $this->discriminatorFieldName], + ), + ), + ]); + + $discriminatorRawValue = $builder->arrayDimFetch($value, $builder->val($this->discriminatorFieldName)); + $discriminatorPath = $builder->arrayImmutableAppend($path, $builder->val($this->discriminatorFieldName)); + $discriminatorMapperMethodName = $builder->uniqMethodName('map' . ucfirst($this->discriminatorFieldName)); + $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, $this->discriminatorMapper)->makePrivate()->getNode(); + $discriminatorMapperCall = $builder->methodCall($builder->var('this'), $discriminatorMapperMethodName, [$discriminatorRawValue, $discriminatorPath]); + $builder->addMethod($discriminatorMapperMethod); + + $validMappingKeys = array_keys($this->objectMappers); + $isDiscriminatorValid = $builder->funcCall($builder->importFunction('in_array'), [$discriminatorRawValue, $builder->val($validMappingKeys), $builder->val(true)]); + + $expectedDescription = $builder->concat( + 'one of ', + $builder->funcCall($builder->importFunction('implode'), [ + ', ', + $builder->val($validMappingKeys), + ]), + ); + + $statements[] = $builder->if($builder->not($isDiscriminatorValid), [ + $builder->throw( + $builder->staticCall( + $builder->importClass(MappingFailedException::class), + 'incorrectValue', + [$discriminatorRawValue, $discriminatorPath, $expectedDescription], + ), + ), + ]); + + $selectedMapperMethodName = $builder->arrayDimFetch($builder->classConstFetch('self', $validMappingKeysConstName), $discriminatorMapperCall); + + return new CompiledExpr( + $builder->methodCall($builder->var('this'), $selectedMapperMethodName, [$value, $path]), + $statements, + ); + } + + public function getInputType(): TypeNode + { + return new IdentifierTypeNode('mixed'); + } + + public function getOutputType(): TypeNode + { + $outputType = new IdentifierTypeNode($this->className); + + if (count($this->genericParameters) === 0) { + return $outputType; + } + + return new GenericTypeNode( + $outputType, + Arrays::map($this->genericParameters, static function (GenericTypeParameter $parameter): TypeNode { + return new IdentifierTypeNode($parameter->name); + }), + ); + } + + /** + * @return list + */ + public function getGenericParameters(): array + { + return $this->genericParameters; + } + +} diff --git a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php index 14be3c7..3c77959 100644 --- a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php +++ b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php @@ -37,7 +37,9 @@ use ShipMonk\InputMapper\Compiler\Mapper\Mixed\MapMixed; use ShipMonk\InputMapper\Compiler\Mapper\Object\AllowExtraKeys; use ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler; +use ShipMonk\InputMapper\Compiler\Mapper\Object\Discriminator; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDateTimeImmutable; +use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapEnum; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapObject; use ShipMonk\InputMapper\Compiler\Mapper\Object\SourceKey; @@ -81,7 +83,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory final public const DEFAULT_VALUE = 'defaultValue'; /** - * @param array): MapperCompiler> $mapperCompilerFactories + * @param array): MapperCompiler> $mapperCompilerFactories */ public function __construct( protected readonly Lexer $phpDocLexer, @@ -95,8 +97,8 @@ public function __construct( /** * @template T of object - * @param class-string $className - * @param callable(class-string, array): MapperCompiler $factory + * @param class-string $className + * @param callable(class-string, array): MapperCompiler $factory */ public function setMapperCompilerFactory(string $className, callable $factory): void { @@ -104,7 +106,7 @@ public function setMapperCompilerFactory(string $className, callable $factory): } /** - * @param array $options + * @param array $options */ public function create(TypeNode $type, array $options = []): MapperCompiler { @@ -281,11 +283,16 @@ protected function createObjectMapperCompiler(string $inputClassName, array $opt } } + $classReflection = new ReflectionClass($inputClassName); + + foreach ($classReflection->getAttributes(Discriminator::class) as $discriminatorAttribute) { + return $this->createDiscriminatorObjectMapping($inputClassName, $discriminatorAttribute->newInstance(), $options); + } + return $this->createObjectMappingByConstructorInvocation($inputClassName, $options); } /** - * @param class-string $inputClassName * @param array $options */ protected function createObjectMappingByConstructorInvocation( @@ -293,8 +300,8 @@ protected function createObjectMappingByConstructorInvocation( array $options, ): MapperCompiler { - $inputType = new IdentifierTypeNode($inputClassName); $classReflection = new ReflectionClass($inputClassName); + $inputType = new IdentifierTypeNode($inputClassName); $constructor = $classReflection->getConstructor(); if ($constructor === null) { @@ -328,7 +335,31 @@ protected function createObjectMappingByConstructorInvocation( } /** - * @param list $genericParameterNames + * @param class-string $inputClassName + * @param array $options + */ + public function createDiscriminatorObjectMapping( + string $inputClassName, + Discriminator $discriminatorAttribute, + array $options, + ): MapperCompiler + { + $objectMappers = []; + + foreach ($discriminatorAttribute->mapping as $key => $mappingClass) { + $objectMappers[$key] = $this->createObjectMapperCompiler($mappingClass, $options); + } + + return new MapDiscriminatedObject( + $inputClassName, + new MapString(), + $discriminatorAttribute->key, + $objectMappers, + ); + } + + /** + * @param list $genericParameterNames * @return array */ protected function getConstructorParameterTypes(ReflectionMethod $constructor, array $genericParameterNames): array @@ -381,7 +412,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor, a } /** - * @param array $options + * @param array $options */ protected function createParameterMapperCompiler( ReflectionParameter $parameterReflection, @@ -455,8 +486,8 @@ protected function addValidator( } /** - * @param class-string $enumName - * @param array $options + * @param class-string $enumName + * @param array $options */ protected function createEnumMapperCompiler(string $enumName, array $options): MapperCompiler { @@ -469,8 +500,8 @@ protected function createEnumMapperCompiler(string $enumName, array $options): M } /** - * @param class-string $className - * @param array $options + * @param class-string $className + * @param array $options */ protected function createDateTimeMapperCompiler(string $className, array $options): MapperCompiler { diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalChildOneInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalChildOneInput.php new file mode 100644 index 0000000..ff570f3 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalChildOneInput.php @@ -0,0 +1,24 @@ + $age + */ + public function __construct( + int $id, + string $name, + Optional $age, + string $type, + public readonly string $childOneField, + ) + { + parent::__construct($id, $name, $age, $type); + } + +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalChildTwoInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalChildTwoInput.php new file mode 100644 index 0000000..b2f9129 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalChildTwoInput.php @@ -0,0 +1,24 @@ + $age + */ + public function __construct( + int $id, + string $name, + Optional $age, + string $type, + public readonly int $childTwoField, + ) + { + parent::__construct($id, $name, $age, $type); + } + +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput.php new file mode 100644 index 0000000..b939619 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput.php @@ -0,0 +1,30 @@ + HierarchicalChildOneInput::class, + 'childTwo' => HierarchicalChildTwoInput::class, + ], +)] +abstract class HierarchicalParentInput +{ + + /** + * @param Optional $age + */ + public function __construct( + public readonly int $id, + public readonly string $name, + public readonly Optional $age, + public readonly string $type, + ) + { + } + +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php new file mode 100644 index 0000000..9f4cc1b --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -0,0 +1,283 @@ + + */ +class HierarchicalParentInputMapper implements Mapper +{ + private const VALID_MAPPINGS = ['childOne' => 'mapChildOne', 'childTwo' => 'mapChildTwo']; + + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalParentInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!in_array($data['type'], ['childOne', 'childTwo'], true)) { + throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])); + } + + return $this->{self::VALID_MAPPINGS[$this->mapType3($data['type'], [...$path, 'type'])]}($data, $path); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapName(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAge(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOneField(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOne(mixed $data, array $path = []): HierarchicalChildOneInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('name', $data)) { + throw MappingFailedException::missingKey($path, 'name'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!array_key_exists('childOneField', $data)) { + throw MappingFailedException::missingKey($path, 'childOneField'); + } + + $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childOneField' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalChildOneInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapName($data['name'], [...$path, 'name']), + array_key_exists('age', $data) ? $this->mapAge($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), + $this->mapType($data['type'], [...$path, 'type']), + $this->mapChildOneField($data['childOneField'], [...$path, 'childOneField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId2(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapName2(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAge2(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType2(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildTwoField(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildTwo(mixed $data, array $path = []): HierarchicalChildTwoInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('name', $data)) { + throw MappingFailedException::missingKey($path, 'name'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!array_key_exists('childTwoField', $data)) { + throw MappingFailedException::missingKey($path, 'childTwoField'); + } + + $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childTwoField' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalChildTwoInput( + $this->mapId2($data['id'], [...$path, 'id']), + $this->mapName2($data['name'], [...$path, 'name']), + array_key_exists('age', $data) ? $this->mapAge2($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), + $this->mapType2($data['type'], [...$path, 'type']), + $this->mapChildTwoField($data['childTwoField'], [...$path, 'childTwoField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType3(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumChildInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumChildInput.php new file mode 100644 index 0000000..664a701 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumChildInput.php @@ -0,0 +1,16 @@ +value => HierarchicalWithEnumChildInput::class, + ], +)] +abstract class HierarchicalWithEnumParentInput +{ + + public function __construct( + public readonly int $id, + public readonly HierarchicalWithEnumType $type, + ) + { + } + +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php new file mode 100644 index 0000000..6f93f32 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -0,0 +1,129 @@ + + */ +class HierarchicalWithEnumParentInputMapper implements Mapper +{ + private const VALID_MAPPINGS = ['childOne' => 'mapChildOne']; + + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalWithEnumParentInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!in_array($data['type'], ['childOne'], true)) { + throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])); + } + + return $this->{self::VALID_MAPPINGS[$this->mapType2($data['type'], [...$path, 'type'])]}($data, $path); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType(mixed $data, array $path = []): HierarchicalWithEnumType + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $enum = HierarchicalWithEnumType::tryFrom($data); + + if ($enum === null) { + throw MappingFailedException::incorrectValue($data, $path, 'one of ' . implode(', ', array_column(HierarchicalWithEnumType::cases(), 'value'))); + } + + return $enum; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOne(mixed $data, array $path = []): HierarchicalWithEnumChildInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + $knownKeys = ['id' => true, 'type' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalWithEnumChildInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapType($data['type'], [...$path, 'type']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType2(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumType.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumType.php new file mode 100644 index 0000000..e5d0b40 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumType.php @@ -0,0 +1,10 @@ + + */ +class ParentMapper implements Mapper +{ + private const VALID_MAPPINGS = ['childOne' => 'mapChildOne', 'childTwo' => 'mapChildTwo']; + + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalParentInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!in_array($data['type'], ['childOne', 'childTwo'], true)) { + throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])); + } + + return $this->{self::VALID_MAPPINGS[$this->mapType3($data['type'], [...$path, 'type'])]}($data, $path); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapName(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAge(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOneField(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOne(mixed $data, array $path = []): HierarchicalChildOneInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('name', $data)) { + throw MappingFailedException::missingKey($path, 'name'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!array_key_exists('childOneField', $data)) { + throw MappingFailedException::missingKey($path, 'childOneField'); + } + + $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childOneField' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalChildOneInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapName($data['name'], [...$path, 'name']), + array_key_exists('age', $data) ? $this->mapAge($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), + $this->mapType($data['type'], [...$path, 'type']), + $this->mapChildOneField($data['childOneField'], [...$path, 'childOneField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId2(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapName2(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAge2(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType2(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildTwoField(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildTwo(mixed $data, array $path = []): HierarchicalChildTwoInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('name', $data)) { + throw MappingFailedException::missingKey($path, 'name'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!array_key_exists('childTwoField', $data)) { + throw MappingFailedException::missingKey($path, 'childTwoField'); + } + + $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childTwoField' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalChildTwoInput( + $this->mapId2($data['id'], [...$path, 'id']), + $this->mapName2($data['name'], [...$path, 'name']), + array_key_exists('age', $data) ? $this->mapAge2($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), + $this->mapType2($data['type'], [...$path, 'type']), + $this->mapChildTwoField($data['childTwoField'], [...$path, 'childTwoField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType3(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php new file mode 100644 index 0000000..9a71ad0 --- /dev/null +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -0,0 +1,174 @@ +compileMapper('HierarchicalParentInput', $this->createParentInputMapperCompiler()); + + $childOneInputObject = new HierarchicalChildOneInput( + id: 1, + name: 'John Doe', + age: Optional::of(30), + type: 'childOne', + childOneField: 'childOneField', + ); + + $childOneInputArray = [ + 'id' => 1, + 'name' => 'John Doe', + 'type' => 'childOne', + 'age' => 30, + 'childOneField' => 'childOneField', + ]; + + self::assertEquals($childOneInputObject, $parentInputMapper->map($childOneInputArray)); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /: Expected array, got null', + static fn() => $parentInputMapper->map(null), + ); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /: Expected array, got 123', + static fn() => $parentInputMapper->map(123), + ); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /: Unrecognized key "extra"', + static fn() => $parentInputMapper->map($childOneInputArray + ['extra' => 1]), + ); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /type: Expected one of childOne, childTwo, got null', + static fn() => $parentInputMapper->map([...$childOneInputArray, 'type' => null]), + ); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /type: Expected one of childOne, childTwo, got "c"', + static fn() => $parentInputMapper->map([...$childOneInputArray, 'type' => 'c']), + ); + + $childOneInputWithoutType = $childOneInputArray; + unset($childOneInputWithoutType['type']); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /: Missing required key "type"', + static fn() => $parentInputMapper->map($childOneInputWithoutType), + ); + + $childTwoInputObject = new HierarchicalChildTwoInput( + id: 1, + name: 'John Doe', + age: Optional::of(30), + type: 'childTwo', + childTwoField: 5, + ); + + $childTwoInputArray = [ + 'id' => 1, + 'name' => 'John Doe', + 'type' => 'childTwo', + 'age' => 30, + 'childTwoField' => 5, + ]; + + self::assertEquals($childTwoInputObject, $parentInputMapper->map($childTwoInputArray)); + } + + public function testCompileWithEnumAsType(): void + { + $parentInputMapper = $this->compileMapper('HierarchicalWithEnumParentInput', $this->createParentInputWithEnumMapperCompiler()); + + $childOneInputObject = new HierarchicalWithEnumChildInput( + id: 1, + type: HierarchicalWithEnumType::ChildOne, + ); + + $childOneInputArray = [ + 'id' => 1, + 'type' => 'childOne', + ]; + + self::assertEquals($childOneInputObject, $parentInputMapper->map($childOneInputArray)); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /type: Expected one of childOne, got null', + static fn() => $parentInputMapper->map([...$childOneInputArray, 'type' => null]), + ); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /type: Expected one of childOne, got "c"', + static fn() => $parentInputMapper->map([...$childOneInputArray, 'type' => 'c']), + ); + } + + private function createParentInputMapperCompiler(): MapperCompiler + { + return new MapDiscriminatedObject( + HierarchicalParentInput::class, + new MapString(), + 'type', + [ + 'childOne' => new MapObject(HierarchicalChildOneInput::class, [ + 'id' => new MapInt(), + 'name' => new MapString(), + 'age' => new MapOptional(new MapInt()), + 'type' => new MapString(), + 'childOneField' => new MapString(), + ]), + 'childTwo' => new MapObject(HierarchicalChildTwoInput::class, [ + 'id' => new MapInt(), + 'name' => new MapString(), + 'age' => new MapOptional(new MapInt()), + 'type' => new MapString(), + 'childTwoField' => new MapInt(), + ]), + ], + ); + } + + private function createParentInputWithEnumMapperCompiler(): MapperCompiler + { + return new MapDiscriminatedObject( + HierarchicalWithEnumParentInput::class, + new MapString(), + 'type', + [ + 'childOne' => new MapObject(HierarchicalWithEnumChildInput::class, [ + 'id' => new MapInt(), + 'type' => new MapEnum(HierarchicalWithEnumType::class, new MapString()), + ]), + ], + ); + } + +} diff --git a/tests/Compiler/MapperFactory/Data/AnimalCatInput.php b/tests/Compiler/MapperFactory/Data/AnimalCatInput.php new file mode 100644 index 0000000..2d55307 --- /dev/null +++ b/tests/Compiler/MapperFactory/Data/AnimalCatInput.php @@ -0,0 +1,16 @@ +value => AnimalCatInput::class, + AnimalType::Dog->value => AnimalDogInput::class, + ], +)] +abstract class AnimalInput +{ + + public function __construct( + public readonly int $id, + public readonly AnimalType $type, + ) + { + } + +} diff --git a/tests/Compiler/MapperFactory/Data/AnimalType.php b/tests/Compiler/MapperFactory/Data/AnimalType.php new file mode 100644 index 0000000..015eaba --- /dev/null +++ b/tests/Compiler/MapperFactory/Data/AnimalType.php @@ -0,0 +1,13 @@ + [ + AnimalInput::class, + [], + new MapDiscriminatedObject( + className: AnimalInput::class, + discriminatorMapper: new MapString(), + discriminatorFieldName: 'type', + objectMappers: [ + AnimalType::Cat->value => new MapObject(AnimalCatInput::class, [ + 'id' => new MapInt(), + 'type' => new DelegateMapperCompiler(AnimalType::class), + ]), + AnimalType::Dog->value => new MapObject(AnimalDogInput::class, [ + 'id' => new MapInt(), + 'type' => new DelegateMapperCompiler(AnimalType::class), + 'dogField' => new MapString(), + ]), + ], + ), + ]; + yield 'ColorEnum' => [ ColorEnum::class, [], From 601440aecbff82c5293a32b2d6c8c9406aafad1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 19:13:51 +0200 Subject: [PATCH 02/16] Drop discriminatorMapper as its always string --- src/Compiler/Mapper/Object/MapDiscriminatedObject.php | 3 +-- src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php | 1 - tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php | 2 -- .../MapperFactory/DefaultMapperCompilerFactoryTest.php | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index a950d37..3835aa9 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -33,7 +33,6 @@ class MapDiscriminatedObject implements GenericMapperCompiler */ public function __construct( public readonly string $className, - public readonly MapString $discriminatorMapper, public readonly string $discriminatorFieldName, public readonly array $objectMappers, public readonly array $genericParameters = [], @@ -84,7 +83,7 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $discriminatorRawValue = $builder->arrayDimFetch($value, $builder->val($this->discriminatorFieldName)); $discriminatorPath = $builder->arrayImmutableAppend($path, $builder->val($this->discriminatorFieldName)); $discriminatorMapperMethodName = $builder->uniqMethodName('map' . ucfirst($this->discriminatorFieldName)); - $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, $this->discriminatorMapper)->makePrivate()->getNode(); + $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, new MapString())->makePrivate()->getNode(); $discriminatorMapperCall = $builder->methodCall($builder->var('this'), $discriminatorMapperMethodName, [$discriminatorRawValue, $discriminatorPath]); $builder->addMethod($discriminatorMapperMethod); diff --git a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php index 3c77959..2e6427e 100644 --- a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php +++ b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php @@ -352,7 +352,6 @@ public function createDiscriminatorObjectMapping( return new MapDiscriminatedObject( $inputClassName, - new MapString(), $discriminatorAttribute->key, $objectMappers, ); diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php index 9a71ad0..c66dbcd 100644 --- a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -135,7 +135,6 @@ private function createParentInputMapperCompiler(): MapperCompiler { return new MapDiscriminatedObject( HierarchicalParentInput::class, - new MapString(), 'type', [ 'childOne' => new MapObject(HierarchicalChildOneInput::class, [ @@ -160,7 +159,6 @@ private function createParentInputWithEnumMapperCompiler(): MapperCompiler { return new MapDiscriminatedObject( HierarchicalWithEnumParentInput::class, - new MapString(), 'type', [ 'childOne' => new MapObject(HierarchicalWithEnumChildInput::class, [ diff --git a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php index a4d7cc0..f26806d 100644 --- a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php +++ b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php @@ -181,7 +181,6 @@ className: CarFilterInput::class, [], new MapDiscriminatedObject( className: AnimalInput::class, - discriminatorMapper: new MapString(), discriminatorFieldName: 'type', objectMappers: [ AnimalType::Cat->value => new MapObject(AnimalCatInput::class, [ From 72aa74532d20d9cba38f4eeb7ead17bb4102b0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 19:22:14 +0200 Subject: [PATCH 03/16] Rewrite mapper to use match instead of constant fetch --- .../Mapper/Object/MapDiscriminatedObject.php | 21 +- src/Compiler/Php/PhpCodeBuilder.php | 18 ++ .../Data/HierarchicalParentInputMapper.php | 7 +- .../Data/HierarchicalWithEnumParentInput.php | 1 - .../HierarchicalWithEnumParentInputMapper.php | 6 +- .../Mapper/Object/Data/ParentMapper.php | 283 ------------------ 6 files changed, 39 insertions(+), 297 deletions(-) delete mode 100644 tests/Compiler/Mapper/Object/Data/ParentMapper.php diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index 3835aa9..5d597b6 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -42,19 +42,17 @@ public function __construct( public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr { - $objectMapperMethods = []; + $objectMapperMethodCalls = []; foreach ($this->objectMappers as $key => $objectMapper) { $objectMapperMethodName = $builder->uniqMethodName('map' . ucfirst($key)); $objectMapperMethod = $builder->mapperMethod($objectMapperMethodName, $objectMapper)->makePrivate()->getNode(); - $objectMapperMethods[$key] = $objectMapperMethodName; + $objectMapperMethodCall = $builder->methodCall($builder->var('this'), $objectMapperMethodName, [$value, $path]); + $objectMapperMethodCalls[$key] = $objectMapperMethodCall; $builder->addMethod($objectMapperMethod); } - $validMappingKeysConstName = $builder->uniqConstantName('VALID_MAPPINGS', $objectMapperMethods); - $builder->addConstant($validMappingKeysConstName, $objectMapperMethods); - $statements = [ $builder->if($builder->not($builder->funcCall($builder->importFunction('is_array'), [$value])), [ $builder->throw( @@ -108,10 +106,19 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi ), ]); - $selectedMapperMethodName = $builder->arrayDimFetch($builder->classConstFetch('self', $validMappingKeysConstName), $discriminatorMapperCall); + $subtypeMatchArms = []; + + foreach ($objectMapperMethodCalls as $key => $objectMapperMethodCall) { + $subtypeMatchArms[] = $builder->matchArm( + $builder->val($key), + $objectMapperMethodCall, + ); + } + + $matchedSubtype = $builder->match($discriminatorMapperCall, $subtypeMatchArms); return new CompiledExpr( - $builder->methodCall($builder->var('this'), $selectedMapperMethodName, [$value, $path]), + $matchedSubtype, $statements, ); } diff --git a/src/Compiler/Php/PhpCodeBuilder.php b/src/Compiler/Php/PhpCodeBuilder.php index bfd8cc7..ca93e82 100644 --- a/src/Compiler/Php/PhpCodeBuilder.php +++ b/src/Compiler/Php/PhpCodeBuilder.php @@ -24,8 +24,10 @@ use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual; use PhpParser\Node\Expr\BooleanNot; use PhpParser\Node\Expr\Instanceof_; +use PhpParser\Node\Expr\Match_; use PhpParser\Node\Expr\PreInc; use PhpParser\Node\Expr\Ternary; +use PhpParser\Node\MatchArm; use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_ as ClassNode; @@ -216,6 +218,22 @@ public function if(Expr $if, array $then, ?array $else = null): If_ return new If_($if, ['stmts' => $then, 'elseifs' => $elseIfClauses, 'else' => $elseClause]); } + /** + * @param list $arms + */ + public function match(Expr $cond, array $arms = []): Match_ + { + return new Match_($cond, $arms); + } + + public function matchArm(?Expr $cond, Expr $body): MatchArm + { + return new MatchArm( + $cond !== null ? [$cond] : null, + $body, + ); + } + /** * @param list $statements */ diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index 9f4cc1b..9712882 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -25,8 +25,6 @@ */ class HierarchicalParentInputMapper implements Mapper { - private const VALID_MAPPINGS = ['childOne' => 'mapChildOne', 'childTwo' => 'mapChildTwo']; - public function __construct(private readonly MapperProvider $provider) { } @@ -49,7 +47,10 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])); } - return $this->{self::VALID_MAPPINGS[$this->mapType3($data['type'], [...$path, 'type'])]}($data, $path); + return match ($this->mapType3($data['type'], [...$path, 'type'])) { + 'childOne' => $this->mapChildOne($data, $path), + 'childTwo' => $this->mapChildTwo($data, $path), + }; } /** diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php index 2b0df5d..47f4541 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php @@ -3,7 +3,6 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data; use ShipMonk\InputMapper\Compiler\Mapper\Object\Discriminator; -use ShipMonk\InputMapper\Runtime\Optional; #[Discriminator( 'type', diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index 6f93f32..e00f517 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -24,8 +24,6 @@ */ class HierarchicalWithEnumParentInputMapper implements Mapper { - private const VALID_MAPPINGS = ['childOne' => 'mapChildOne']; - public function __construct(private readonly MapperProvider $provider) { } @@ -48,7 +46,9 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])); } - return $this->{self::VALID_MAPPINGS[$this->mapType2($data['type'], [...$path, 'type'])]}($data, $path); + return match ($this->mapType2($data['type'], [...$path, 'type'])) { + 'childOne' => $this->mapChildOne($data, $path), + }; } /** diff --git a/tests/Compiler/Mapper/Object/Data/ParentMapper.php b/tests/Compiler/Mapper/Object/Data/ParentMapper.php deleted file mode 100644 index 8e011f9..0000000 --- a/tests/Compiler/Mapper/Object/Data/ParentMapper.php +++ /dev/null @@ -1,283 +0,0 @@ - - */ -class ParentMapper implements Mapper -{ - private const VALID_MAPPINGS = ['childOne' => 'mapChildOne', 'childTwo' => 'mapChildTwo']; - - public function __construct(private readonly MapperProvider $provider) - { - } - - /** - * @param list $path - * @throws MappingFailedException - */ - public function map(mixed $data, array $path = []): HierarchicalParentInput - { - if (!is_array($data)) { - throw MappingFailedException::incorrectType($data, $path, 'array'); - } - - if (!array_key_exists('type', $data)) { - throw MappingFailedException::missingKey($path, 'type'); - } - - if (!in_array($data['type'], ['childOne', 'childTwo'], true)) { - throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])); - } - - return $this->{self::VALID_MAPPINGS[$this->mapType3($data['type'], [...$path, 'type'])]}($data, $path); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapId(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapName(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @return OptionalSome - * @throws MappingFailedException - */ - private function mapAge(mixed $data, array $path = []): OptionalSome - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return Optional::of($data); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildOneField(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildOne(mixed $data, array $path = []): HierarchicalChildOneInput - { - if (!is_array($data)) { - throw MappingFailedException::incorrectType($data, $path, 'array'); - } - - if (!array_key_exists('id', $data)) { - throw MappingFailedException::missingKey($path, 'id'); - } - - if (!array_key_exists('name', $data)) { - throw MappingFailedException::missingKey($path, 'name'); - } - - if (!array_key_exists('type', $data)) { - throw MappingFailedException::missingKey($path, 'type'); - } - - if (!array_key_exists('childOneField', $data)) { - throw MappingFailedException::missingKey($path, 'childOneField'); - } - - $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childOneField' => true]; - $extraKeys = array_diff_key($data, $knownKeys); - - if (count($extraKeys) > 0) { - throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); - } - - return new HierarchicalChildOneInput( - $this->mapId($data['id'], [...$path, 'id']), - $this->mapName($data['name'], [...$path, 'name']), - array_key_exists('age', $data) ? $this->mapAge($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), - $this->mapType($data['type'], [...$path, 'type']), - $this->mapChildOneField($data['childOneField'], [...$path, 'childOneField']), - ); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapId2(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapName2(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @return OptionalSome - * @throws MappingFailedException - */ - private function mapAge2(mixed $data, array $path = []): OptionalSome - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return Optional::of($data); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType2(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildTwoField(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildTwo(mixed $data, array $path = []): HierarchicalChildTwoInput - { - if (!is_array($data)) { - throw MappingFailedException::incorrectType($data, $path, 'array'); - } - - if (!array_key_exists('id', $data)) { - throw MappingFailedException::missingKey($path, 'id'); - } - - if (!array_key_exists('name', $data)) { - throw MappingFailedException::missingKey($path, 'name'); - } - - if (!array_key_exists('type', $data)) { - throw MappingFailedException::missingKey($path, 'type'); - } - - if (!array_key_exists('childTwoField', $data)) { - throw MappingFailedException::missingKey($path, 'childTwoField'); - } - - $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childTwoField' => true]; - $extraKeys = array_diff_key($data, $knownKeys); - - if (count($extraKeys) > 0) { - throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); - } - - return new HierarchicalChildTwoInput( - $this->mapId2($data['id'], [...$path, 'id']), - $this->mapName2($data['name'], [...$path, 'name']), - array_key_exists('age', $data) ? $this->mapAge2($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), - $this->mapType2($data['type'], [...$path, 'type']), - $this->mapChildTwoField($data['childTwoField'], [...$path, 'childTwoField']), - ); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType3(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } -} From 655374cf52b794767f11968e645c8b6759d1e869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 19:32:32 +0200 Subject: [PATCH 04/16] Do not reference enum in constants in tests - compatibility with 8.1 --- .../Mapper/Object/Data/HierarchicalWithEnumParentInput.php | 2 +- tests/Compiler/MapperFactory/Data/AnimalInput.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php index 47f4541..2cfa402 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput.php @@ -7,7 +7,7 @@ #[Discriminator( 'type', mapping: [ - HierarchicalWithEnumType::ChildOne->value => HierarchicalWithEnumChildInput::class, + 'childOne' => HierarchicalWithEnumChildInput::class, ], )] abstract class HierarchicalWithEnumParentInput diff --git a/tests/Compiler/MapperFactory/Data/AnimalInput.php b/tests/Compiler/MapperFactory/Data/AnimalInput.php index 1d1fff3..519000d 100644 --- a/tests/Compiler/MapperFactory/Data/AnimalInput.php +++ b/tests/Compiler/MapperFactory/Data/AnimalInput.php @@ -7,8 +7,8 @@ #[Discriminator( 'type', mapping: [ - AnimalType::Cat->value => AnimalCatInput::class, - AnimalType::Dog->value => AnimalDogInput::class, + 'cat' => AnimalCatInput::class, + 'dog' => AnimalDogInput::class, ], )] abstract class AnimalInput From 51bbc5c8ee0ab39fd45aeec6aa953ca2c686221a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 19:49:58 +0200 Subject: [PATCH 05/16] Rewrite to MapperProvider delegation --- .../Mapper/Object/MapDiscriminatedObject.php | 26 +- .../DefaultMapperCompilerFactory.php | 14 +- .../Data/HierarchicalParentInputMapper.php | 228 +----------------- ...Input__HierarchicalChildOneInputMapper.php | 137 +++++++++++ ...Input__HierarchicalChildTwoInputMapper.php | 137 +++++++++++ .../HierarchicalWithEnumParentInputMapper.php | 74 +----- ...__HierarchicalWithEnumChildInputMapper.php | 92 +++++++ .../Object/MapDiscriminatedObjectTest.php | 60 +++-- .../DefaultMapperCompilerFactoryTest.php | 13 +- 9 files changed, 430 insertions(+), 351 deletions(-) create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildOneInputMapper.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildTwoInputMapper.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput__HierarchicalWithEnumChildInputMapper.php diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index 5d597b6..953b3da 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -10,7 +10,6 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ShipMonk\InputMapper\Compiler\CompiledExpr; use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler; -use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder; use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter; @@ -28,13 +27,13 @@ class MapDiscriminatedObject implements GenericMapperCompiler /** * @param class-string $className - * @param array $objectMappers + * @param array $subtypeMapping * @param list $genericParameters */ public function __construct( public readonly string $className, public readonly string $discriminatorFieldName, - public readonly array $objectMappers, + public readonly array $subtypeMapping, public readonly array $genericParameters = [], ) { @@ -42,16 +41,7 @@ public function __construct( public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr { - $objectMapperMethodCalls = []; - - foreach ($this->objectMappers as $key => $objectMapper) { - $objectMapperMethodName = $builder->uniqMethodName('map' . ucfirst($key)); - $objectMapperMethod = $builder->mapperMethod($objectMapperMethodName, $objectMapper)->makePrivate()->getNode(); - $objectMapperMethodCall = $builder->methodCall($builder->var('this'), $objectMapperMethodName, [$value, $path]); - $objectMapperMethodCalls[$key] = $objectMapperMethodCall; - - $builder->addMethod($objectMapperMethod); - } + $provider = $builder->propertyFetch($builder->var('this'), 'provider'); $statements = [ $builder->if($builder->not($builder->funcCall($builder->importFunction('is_array'), [$value])), [ @@ -85,7 +75,7 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $discriminatorMapperCall = $builder->methodCall($builder->var('this'), $discriminatorMapperMethodName, [$discriminatorRawValue, $discriminatorPath]); $builder->addMethod($discriminatorMapperMethod); - $validMappingKeys = array_keys($this->objectMappers); + $validMappingKeys = array_keys($this->subtypeMapping); $isDiscriminatorValid = $builder->funcCall($builder->importFunction('in_array'), [$discriminatorRawValue, $builder->val($validMappingKeys), $builder->val(true)]); $expectedDescription = $builder->concat( @@ -108,10 +98,14 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $subtypeMatchArms = []; - foreach ($objectMapperMethodCalls as $key => $objectMapperMethodCall) { + foreach ($this->subtypeMapping as $key => $subtype) { + $mapperProviderMethodCall = $builder->methodCall($provider, 'get', [ + $builder->classConstFetch($builder->importClass($subtype), 'class'), + ]); + $subtypeMatchArms[] = $builder->matchArm( $builder->val($key), - $objectMapperMethodCall, + $builder->methodCall($mapperProviderMethodCall, 'map', [$value, $path]), ); } diff --git a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php index 2e6427e..2abcf01 100644 --- a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php +++ b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php @@ -286,7 +286,7 @@ protected function createObjectMapperCompiler(string $inputClassName, array $opt $classReflection = new ReflectionClass($inputClassName); foreach ($classReflection->getAttributes(Discriminator::class) as $discriminatorAttribute) { - return $this->createDiscriminatorObjectMapping($inputClassName, $discriminatorAttribute->newInstance(), $options); + return $this->createDiscriminatorObjectMapping($inputClassName, $discriminatorAttribute->newInstance()); } return $this->createObjectMappingByConstructorInvocation($inputClassName, $options); @@ -336,24 +336,20 @@ protected function createObjectMappingByConstructorInvocation( /** * @param class-string $inputClassName - * @param array $options */ public function createDiscriminatorObjectMapping( string $inputClassName, Discriminator $discriminatorAttribute, - array $options, ): MapperCompiler { - $objectMappers = []; - - foreach ($discriminatorAttribute->mapping as $key => $mappingClass) { - $objectMappers[$key] = $this->createObjectMapperCompiler($mappingClass, $options); - } + $inputType = new IdentifierTypeNode($inputClassName); + $genericParameters = PhpDocTypeUtils::getGenericTypeDefinition($inputType)->parameters; return new MapDiscriminatedObject( $inputClassName, $discriminatorAttribute->key, - $objectMappers, + $discriminatorAttribute->mapping, + $genericParameters, ); } diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index 9712882..3c926a6 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -6,16 +6,10 @@ use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; -use ShipMonk\InputMapper\Runtime\Optional; -use ShipMonk\InputMapper\Runtime\OptionalSome; -use function array_diff_key; use function array_key_exists; -use function array_keys; -use function count; use function implode; use function in_array; use function is_array; -use function is_int; use function is_string; /** @@ -47,52 +41,12 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])); } - return match ($this->mapType3($data['type'], [...$path, 'type'])) { - 'childOne' => $this->mapChildOne($data, $path), - 'childTwo' => $this->mapChildTwo($data, $path), + return match ($this->mapType($data['type'], [...$path, 'type'])) { + 'childOne' => $this->provider->get(HierarchicalChildOneInput::class)->map($data, $path), + 'childTwo' => $this->provider->get(HierarchicalChildTwoInput::class)->map($data, $path), }; } - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapId(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapName(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @return OptionalSome - * @throws MappingFailedException - */ - private function mapAge(mixed $data, array $path = []): OptionalSome - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return Optional::of($data); - } - /** * @param list $path * @throws MappingFailedException @@ -105,180 +59,4 @@ private function mapType(mixed $data, array $path = []): string return $data; } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildOneField(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildOne(mixed $data, array $path = []): HierarchicalChildOneInput - { - if (!is_array($data)) { - throw MappingFailedException::incorrectType($data, $path, 'array'); - } - - if (!array_key_exists('id', $data)) { - throw MappingFailedException::missingKey($path, 'id'); - } - - if (!array_key_exists('name', $data)) { - throw MappingFailedException::missingKey($path, 'name'); - } - - if (!array_key_exists('type', $data)) { - throw MappingFailedException::missingKey($path, 'type'); - } - - if (!array_key_exists('childOneField', $data)) { - throw MappingFailedException::missingKey($path, 'childOneField'); - } - - $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childOneField' => true]; - $extraKeys = array_diff_key($data, $knownKeys); - - if (count($extraKeys) > 0) { - throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); - } - - return new HierarchicalChildOneInput( - $this->mapId($data['id'], [...$path, 'id']), - $this->mapName($data['name'], [...$path, 'name']), - array_key_exists('age', $data) ? $this->mapAge($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), - $this->mapType($data['type'], [...$path, 'type']), - $this->mapChildOneField($data['childOneField'], [...$path, 'childOneField']), - ); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapId2(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapName2(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @return OptionalSome - * @throws MappingFailedException - */ - private function mapAge2(mixed $data, array $path = []): OptionalSome - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return Optional::of($data); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType2(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildTwoField(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildTwo(mixed $data, array $path = []): HierarchicalChildTwoInput - { - if (!is_array($data)) { - throw MappingFailedException::incorrectType($data, $path, 'array'); - } - - if (!array_key_exists('id', $data)) { - throw MappingFailedException::missingKey($path, 'id'); - } - - if (!array_key_exists('name', $data)) { - throw MappingFailedException::missingKey($path, 'name'); - } - - if (!array_key_exists('type', $data)) { - throw MappingFailedException::missingKey($path, 'type'); - } - - if (!array_key_exists('childTwoField', $data)) { - throw MappingFailedException::missingKey($path, 'childTwoField'); - } - - $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childTwoField' => true]; - $extraKeys = array_diff_key($data, $knownKeys); - - if (count($extraKeys) > 0) { - throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); - } - - return new HierarchicalChildTwoInput( - $this->mapId2($data['id'], [...$path, 'id']), - $this->mapName2($data['name'], [...$path, 'name']), - array_key_exists('age', $data) ? $this->mapAge2($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), - $this->mapType2($data['type'], [...$path, 'type']), - $this->mapChildTwoField($data['childTwoField'], [...$path, 'childTwoField']), - ); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType3(mixed $data, array $path = []): string - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - return $data; - } } diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildOneInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildOneInputMapper.php new file mode 100644 index 0000000..b41e9e6 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildOneInputMapper.php @@ -0,0 +1,137 @@ + + */ +class HierarchicalParentInput__HierarchicalChildOneInputMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalChildOneInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('name', $data)) { + throw MappingFailedException::missingKey($path, 'name'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!array_key_exists('childOneField', $data)) { + throw MappingFailedException::missingKey($path, 'childOneField'); + } + + $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childOneField' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalChildOneInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapName($data['name'], [...$path, 'name']), + array_key_exists('age', $data) ? $this->mapAge($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), + $this->mapType($data['type'], [...$path, 'type']), + $this->mapChildOneField($data['childOneField'], [...$path, 'childOneField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapName(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAge(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOneField(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildTwoInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildTwoInputMapper.php new file mode 100644 index 0000000..78fa6ef --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInput__HierarchicalChildTwoInputMapper.php @@ -0,0 +1,137 @@ + + */ +class HierarchicalParentInput__HierarchicalChildTwoInputMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalChildTwoInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('name', $data)) { + throw MappingFailedException::missingKey($path, 'name'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + if (!array_key_exists('childTwoField', $data)) { + throw MappingFailedException::missingKey($path, 'childTwoField'); + } + + $knownKeys = ['id' => true, 'name' => true, 'age' => true, 'type' => true, 'childTwoField' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalChildTwoInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapName($data['name'], [...$path, 'name']), + array_key_exists('age', $data) ? $this->mapAge($data['age'], [...$path, 'age']) : Optional::none($path, 'age'), + $this->mapType($data['type'], [...$path, 'type']), + $this->mapChildTwoField($data['childTwoField'], [...$path, 'childTwoField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapName(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAge(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildTwoField(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index e00f517..6e06ee6 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -6,15 +6,10 @@ use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; -use function array_column; -use function array_diff_key; use function array_key_exists; -use function array_keys; -use function count; use function implode; use function in_array; use function is_array; -use function is_int; use function is_string; /** @@ -46,8 +41,8 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])); } - return match ($this->mapType2($data['type'], [...$path, 'type'])) { - 'childOne' => $this->mapChildOne($data, $path), + return match ($this->mapType($data['type'], [...$path, 'type'])) { + 'childOne' => $this->provider->get(HierarchicalWithEnumChildInput::class)->map($data, $path), }; } @@ -55,70 +50,7 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn * @param list $path * @throws MappingFailedException */ - private function mapId(mixed $data, array $path = []): int - { - if (!is_int($data)) { - throw MappingFailedException::incorrectType($data, $path, 'int'); - } - - return $data; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType(mixed $data, array $path = []): HierarchicalWithEnumType - { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - $enum = HierarchicalWithEnumType::tryFrom($data); - - if ($enum === null) { - throw MappingFailedException::incorrectValue($data, $path, 'one of ' . implode(', ', array_column(HierarchicalWithEnumType::cases(), 'value'))); - } - - return $enum; - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapChildOne(mixed $data, array $path = []): HierarchicalWithEnumChildInput - { - if (!is_array($data)) { - throw MappingFailedException::incorrectType($data, $path, 'array'); - } - - if (!array_key_exists('id', $data)) { - throw MappingFailedException::missingKey($path, 'id'); - } - - if (!array_key_exists('type', $data)) { - throw MappingFailedException::missingKey($path, 'type'); - } - - $knownKeys = ['id' => true, 'type' => true]; - $extraKeys = array_diff_key($data, $knownKeys); - - if (count($extraKeys) > 0) { - throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); - } - - return new HierarchicalWithEnumChildInput( - $this->mapId($data['id'], [...$path, 'id']), - $this->mapType($data['type'], [...$path, 'type']), - ); - } - - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapType2(mixed $data, array $path = []): string + private function mapType(mixed $data, array $path = []): string { if (!is_string($data)) { throw MappingFailedException::incorrectType($data, $path, 'string'); diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput__HierarchicalWithEnumChildInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput__HierarchicalWithEnumChildInputMapper.php new file mode 100644 index 0000000..e8893dc --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInput__HierarchicalWithEnumChildInputMapper.php @@ -0,0 +1,92 @@ + + */ +class HierarchicalWithEnumParentInput__HierarchicalWithEnumChildInputMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalWithEnumChildInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('type', $data)) { + throw MappingFailedException::missingKey($path, 'type'); + } + + $knownKeys = ['id' => true, 'type' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new HierarchicalWithEnumChildInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapType($data['type'], [...$path, 'type']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapType(mixed $data, array $path = []): HierarchicalWithEnumType + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $enum = HierarchicalWithEnumType::tryFrom($data); + + if ($enum === null) { + throw MappingFailedException::incorrectValue($data, $path, 'one of ' . implode(', ', array_column(HierarchicalWithEnumType::cases(), 'value'))); + } + + return $enum; + } +} diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php index c66dbcd..95b2cd0 100644 --- a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -24,7 +24,10 @@ class MapDiscriminatedObjectTest extends MapperCompilerTestCase public function testCompile(): void { - $parentInputMapper = $this->compileMapper('HierarchicalParentInput', $this->createParentInputMapperCompiler()); + $parentInputMapper = $this->compileMapper('HierarchicalParentInput', $this->createParentInputMapperCompiler(), [ + HierarchicalChildOneInput::class => $this->createHierarchicalChildOneInputMapperCompiler(), + HierarchicalChildTwoInput::class => $this->createHierarchicalChildTwoInputMapperCompiler(), + ]); $childOneInputObject = new HierarchicalChildOneInput( id: 1, @@ -104,7 +107,9 @@ public function testCompile(): void public function testCompileWithEnumAsType(): void { - $parentInputMapper = $this->compileMapper('HierarchicalWithEnumParentInput', $this->createParentInputWithEnumMapperCompiler()); + $parentInputMapper = $this->compileMapper('HierarchicalWithEnumParentInput', $this->createParentInputWithEnumMapperCompiler(), [ + HierarchicalWithEnumChildInput::class => $this->createHierarchicalChildWithEnumMapperCompiler(), + ]); $childOneInputObject = new HierarchicalWithEnumChildInput( id: 1, @@ -137,20 +142,8 @@ private function createParentInputMapperCompiler(): MapperCompiler HierarchicalParentInput::class, 'type', [ - 'childOne' => new MapObject(HierarchicalChildOneInput::class, [ - 'id' => new MapInt(), - 'name' => new MapString(), - 'age' => new MapOptional(new MapInt()), - 'type' => new MapString(), - 'childOneField' => new MapString(), - ]), - 'childTwo' => new MapObject(HierarchicalChildTwoInput::class, [ - 'id' => new MapInt(), - 'name' => new MapString(), - 'age' => new MapOptional(new MapInt()), - 'type' => new MapString(), - 'childTwoField' => new MapInt(), - ]), + 'childOne' => HierarchicalChildOneInput::class, + 'childTwo' => HierarchicalChildTwoInput::class, ], ); } @@ -161,12 +154,39 @@ private function createParentInputWithEnumMapperCompiler(): MapperCompiler HierarchicalWithEnumParentInput::class, 'type', [ - 'childOne' => new MapObject(HierarchicalWithEnumChildInput::class, [ - 'id' => new MapInt(), - 'type' => new MapEnum(HierarchicalWithEnumType::class, new MapString()), - ]), + 'childOne' => HierarchicalWithEnumChildInput::class, ], ); } + public function createHierarchicalChildOneInputMapperCompiler(): MapperCompiler + { + return new MapObject(HierarchicalChildOneInput::class, [ + 'id' => new MapInt(), + 'name' => new MapString(), + 'age' => new MapOptional(new MapInt()), + 'type' => new MapString(), + 'childOneField' => new MapString(), + ]); + } + + public function createHierarchicalChildTwoInputMapperCompiler(): MapperCompiler + { + return new MapObject(HierarchicalChildTwoInput::class, [ + 'id' => new MapInt(), + 'name' => new MapString(), + 'age' => new MapOptional(new MapInt()), + 'type' => new MapString(), + 'childTwoField' => new MapInt(), + ]); + } + + public function createHierarchicalChildWithEnumMapperCompiler(): MapperCompiler + { + return new MapObject(HierarchicalWithEnumChildInput::class, [ + 'id' => new MapInt(), + 'type' => new MapEnum(HierarchicalWithEnumType::class, new MapString()), + ]); + } + } diff --git a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php index f26806d..2cdfd71 100644 --- a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php +++ b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php @@ -182,16 +182,9 @@ className: CarFilterInput::class, new MapDiscriminatedObject( className: AnimalInput::class, discriminatorFieldName: 'type', - objectMappers: [ - AnimalType::Cat->value => new MapObject(AnimalCatInput::class, [ - 'id' => new MapInt(), - 'type' => new DelegateMapperCompiler(AnimalType::class), - ]), - AnimalType::Dog->value => new MapObject(AnimalDogInput::class, [ - 'id' => new MapInt(), - 'type' => new DelegateMapperCompiler(AnimalType::class), - 'dogField' => new MapString(), - ]), + subtypeMapping: [ + AnimalType::Cat->value => AnimalCatInput::class, + AnimalType::Dog->value => AnimalDogInput::class, ], ), ]; From 4c8b4b4bc05b2e42e5a577cf7282def19cb23e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 23:23:41 +0200 Subject: [PATCH 06/16] Code style diffs --- .../DefaultMapperCompilerFactory.php | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php index 2abcf01..326968b 100644 --- a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php +++ b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php @@ -83,7 +83,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory final public const DEFAULT_VALUE = 'defaultValue'; /** - * @param array): MapperCompiler> $mapperCompilerFactories + * @param array): MapperCompiler> $mapperCompilerFactories */ public function __construct( protected readonly Lexer $phpDocLexer, @@ -97,8 +97,8 @@ public function __construct( /** * @template T of object - * @param class-string $className - * @param callable(class-string, array): MapperCompiler $factory + * @param class-string $className + * @param callable(class-string, array): MapperCompiler $factory */ public function setMapperCompilerFactory(string $className, callable $factory): void { @@ -106,7 +106,7 @@ public function setMapperCompilerFactory(string $className, callable $factory): } /** - * @param array $options + * @param array $options */ public function create(TypeNode $type, array $options = []): MapperCompiler { @@ -293,6 +293,7 @@ protected function createObjectMapperCompiler(string $inputClassName, array $opt } /** + * @param class-string $inputClassName * @param array $options */ protected function createObjectMappingByConstructorInvocation( @@ -300,8 +301,8 @@ protected function createObjectMappingByConstructorInvocation( array $options, ): MapperCompiler { - $classReflection = new ReflectionClass($inputClassName); $inputType = new IdentifierTypeNode($inputClassName); + $classReflection = new ReflectionClass($inputClassName); $constructor = $classReflection->getConstructor(); if ($constructor === null) { @@ -354,7 +355,7 @@ public function createDiscriminatorObjectMapping( } /** - * @param list $genericParameterNames + * @param list $genericParameterNames * @return array */ protected function getConstructorParameterTypes(ReflectionMethod $constructor, array $genericParameterNames): array @@ -407,7 +408,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor, a } /** - * @param array $options + * @param array $options */ protected function createParameterMapperCompiler( ReflectionParameter $parameterReflection, @@ -481,8 +482,8 @@ protected function addValidator( } /** - * @param class-string $enumName - * @param array $options + * @param class-string $enumName + * @param array $options */ protected function createEnumMapperCompiler(string $enumName, array $options): MapperCompiler { @@ -495,8 +496,8 @@ protected function createEnumMapperCompiler(string $enumName, array $options): M } /** - * @param class-string $className - * @param array $options + * @param class-string $className + * @param array $options */ protected function createDateTimeMapperCompiler(string $className, array $options): MapperCompiler { From 1a7b801807e39ab6fd87959eefae497f0e39a46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Fri, 6 Sep 2024 23:28:57 +0200 Subject: [PATCH 07/16] Add default (impossible) branch --- src/Compiler/Mapper/Object/MapDiscriminatedObject.php | 11 +++++++++++ src/Compiler/Php/PhpCodeBuilder.php | 6 ++++++ .../Object/Data/HierarchicalParentInputMapper.php | 2 ++ .../Data/HierarchicalWithEnumParentInputMapper.php | 2 ++ 4 files changed, 21 insertions(+) diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index 953b3da..aa051ec 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -3,6 +3,7 @@ namespace ShipMonk\InputMapper\Compiler\Mapper\Object; use Attribute; +use LogicException; use Nette\Utils\Arrays; use PhpParser\Node\Expr; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; @@ -109,6 +110,16 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi ); } + $subtypeMatchArms[] = $builder->matchArm( + null, + $builder->throwExpr( + $builder->new( + $builder->importClass(LogicException::class), + ['Impossible case detected. Please report this as a bug.'], + ), + ), + ); + $matchedSubtype = $builder->match($discriminatorMapperCall, $subtypeMatchArms); return new CompiledExpr( diff --git a/src/Compiler/Php/PhpCodeBuilder.php b/src/Compiler/Php/PhpCodeBuilder.php index ca93e82..2e0f84f 100644 --- a/src/Compiler/Php/PhpCodeBuilder.php +++ b/src/Compiler/Php/PhpCodeBuilder.php @@ -27,6 +27,7 @@ use PhpParser\Node\Expr\Match_; use PhpParser\Node\Expr\PreInc; use PhpParser\Node\Expr\Ternary; +use PhpParser\Node\Expr\Throw_ as ThrowExpr_; use PhpParser\Node\MatchArm; use PhpParser\Node\Name; use PhpParser\Node\Stmt; @@ -278,6 +279,11 @@ public function throw(Expr $expr): Throw_ return new Throw_($expr); } + public function throwExpr(Expr $expr): ThrowExpr_ + { + return new ThrowExpr_($expr); + } + public function assign(Expr $var, Expr $expr): Expression { return new Expression(new Assign($var, $expr)); diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index 3c926a6..22ee368 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data; +use LogicException; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use ShipMonk\InputMapper\Runtime\Mapper; @@ -44,6 +45,7 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput return match ($this->mapType($data['type'], [...$path, 'type'])) { 'childOne' => $this->provider->get(HierarchicalChildOneInput::class)->map($data, $path), 'childTwo' => $this->provider->get(HierarchicalChildTwoInput::class)->map($data, $path), + default => throw new LogicException('Impossible case detected. Please report this as a bug.'), }; } diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index 6e06ee6..bbb8e21 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data; +use LogicException; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use ShipMonk\InputMapper\Runtime\Mapper; @@ -43,6 +44,7 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn return match ($this->mapType($data['type'], [...$path, 'type'])) { 'childOne' => $this->provider->get(HierarchicalWithEnumChildInput::class)->map($data, $path), + default => throw new LogicException('Impossible case detected. Please report this as a bug.'), }; } From 432b5d1fe0342591468040eef98ea82d2273c2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 12:55:01 +0200 Subject: [PATCH 08/16] Throw incorrectValue in the default match case --- .../Mapper/Object/MapDiscriminatedObject.php | 30 +++++++------------ .../Data/HierarchicalParentInputMapper.php | 22 +++++++------- .../HierarchicalWithEnumParentInputMapper.php | 22 +++++++------- .../DefaultMapperCompilerFactoryTest.php | 2 +- 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index aa051ec..1913b67 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -3,7 +3,6 @@ namespace ShipMonk\InputMapper\Compiler\Mapper\Object; use Attribute; -use LogicException; use Nette\Utils\Arrays; use PhpParser\Node\Expr; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; @@ -12,6 +11,7 @@ use ShipMonk\InputMapper\Compiler\CompiledExpr; use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; +use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable; use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder; use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; @@ -28,13 +28,13 @@ class MapDiscriminatedObject implements GenericMapperCompiler /** * @param class-string $className - * @param array $subtypeMapping + * @param array $subtypeCompilers * @param list $genericParameters */ public function __construct( public readonly string $className, public readonly string $discriminatorFieldName, - public readonly array $subtypeMapping, + public readonly array $subtypeCompilers, public readonly array $genericParameters = [], ) { @@ -72,12 +72,11 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $discriminatorRawValue = $builder->arrayDimFetch($value, $builder->val($this->discriminatorFieldName)); $discriminatorPath = $builder->arrayImmutableAppend($path, $builder->val($this->discriminatorFieldName)); $discriminatorMapperMethodName = $builder->uniqMethodName('map' . ucfirst($this->discriminatorFieldName)); - $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, new MapString())->makePrivate()->getNode(); + $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, new MapNullable(new MapString()))->makePrivate()->getNode(); $discriminatorMapperCall = $builder->methodCall($builder->var('this'), $discriminatorMapperMethodName, [$discriminatorRawValue, $discriminatorPath]); $builder->addMethod($discriminatorMapperMethod); - $validMappingKeys = array_keys($this->subtypeMapping); - $isDiscriminatorValid = $builder->funcCall($builder->importFunction('in_array'), [$discriminatorRawValue, $builder->val($validMappingKeys), $builder->val(true)]); + $validMappingKeys = array_keys($this->subtypeCompilers); $expectedDescription = $builder->concat( 'one of ', @@ -87,19 +86,9 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi ]), ); - $statements[] = $builder->if($builder->not($isDiscriminatorValid), [ - $builder->throw( - $builder->staticCall( - $builder->importClass(MappingFailedException::class), - 'incorrectValue', - [$discriminatorRawValue, $discriminatorPath, $expectedDescription], - ), - ), - ]); - $subtypeMatchArms = []; - foreach ($this->subtypeMapping as $key => $subtype) { + foreach ($this->subtypeCompilers as $key => $subtype) { $mapperProviderMethodCall = $builder->methodCall($provider, 'get', [ $builder->classConstFetch($builder->importClass($subtype), 'class'), ]); @@ -113,9 +102,10 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $subtypeMatchArms[] = $builder->matchArm( null, $builder->throwExpr( - $builder->new( - $builder->importClass(LogicException::class), - ['Impossible case detected. Please report this as a bug.'], + $builder->staticCall( + $builder->importClass(MappingFailedException::class), + 'incorrectValue', + [$discriminatorRawValue, $discriminatorPath, $expectedDescription], ), ), ); diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index 22ee368..9419053 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -2,14 +2,12 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data; -use LogicException; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use function array_key_exists; use function implode; -use function in_array; use function is_array; use function is_string; @@ -38,14 +36,10 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput throw MappingFailedException::missingKey($path, 'type'); } - if (!in_array($data['type'], ['childOne', 'childTwo'], true)) { - throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])); - } - return match ($this->mapType($data['type'], [...$path, 'type'])) { 'childOne' => $this->provider->get(HierarchicalChildOneInput::class)->map($data, $path), 'childTwo' => $this->provider->get(HierarchicalChildTwoInput::class)->map($data, $path), - default => throw new LogicException('Impossible case detected. Please report this as a bug.'), + default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])), }; } @@ -53,12 +47,18 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput * @param list $path * @throws MappingFailedException */ - private function mapType(mixed $data, array $path = []): string + private function mapType(mixed $data, array $path = []): ?string { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); + if ($data === null) { + $mapped = null; + } else { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $mapped = $data; } - return $data; + return $mapped; } } diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index bbb8e21..30ce0a0 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -2,14 +2,12 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data; -use LogicException; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use function array_key_exists; use function implode; -use function in_array; use function is_array; use function is_string; @@ -38,13 +36,9 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn throw MappingFailedException::missingKey($path, 'type'); } - if (!in_array($data['type'], ['childOne'], true)) { - throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])); - } - return match ($this->mapType($data['type'], [...$path, 'type'])) { 'childOne' => $this->provider->get(HierarchicalWithEnumChildInput::class)->map($data, $path), - default => throw new LogicException('Impossible case detected. Please report this as a bug.'), + default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])), }; } @@ -52,12 +46,18 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn * @param list $path * @throws MappingFailedException */ - private function mapType(mixed $data, array $path = []): string + private function mapType(mixed $data, array $path = []): ?string { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); + if ($data === null) { + $mapped = null; + } else { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $mapped = $data; } - return $data; + return $mapped; } } diff --git a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php index 2cdfd71..6d68275 100644 --- a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php +++ b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php @@ -182,7 +182,7 @@ className: CarFilterInput::class, new MapDiscriminatedObject( className: AnimalInput::class, discriminatorFieldName: 'type', - subtypeMapping: [ + subtypeCompilers: [ AnimalType::Cat->value => AnimalCatInput::class, AnimalType::Dog->value => AnimalDogInput::class, ], From d2d7953708a6127537ee479bdb81151080659173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ol=C5=A1avsk=C3=BD?= Date: Mon, 9 Sep 2024 12:55:20 +0200 Subject: [PATCH 09/16] Update src/Compiler/Mapper/Object/Discriminator.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Tvrdík --- src/Compiler/Mapper/Object/Discriminator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Mapper/Object/Discriminator.php b/src/Compiler/Mapper/Object/Discriminator.php index d18e2e8..bce4585 100644 --- a/src/Compiler/Mapper/Object/Discriminator.php +++ b/src/Compiler/Mapper/Object/Discriminator.php @@ -8,11 +8,11 @@ class Discriminator { + /** + * @param array $mapping + */ public function __construct( public readonly string $key, - /** - * @var array - */ public readonly array $mapping ) { From 20e366d0769e30700a00311853bd3d6269380862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 13:09:36 +0200 Subject: [PATCH 10/16] Rewrite to DelegateMapperCompiler with no change to generic behavior --- .../Mapper/Object/MapDiscriminatedObject.php | 17 +++++++------- .../DefaultMapperCompilerFactory.php | 8 ++++++- .../Data/HierarchicalParentInputMapper.php | 22 +++++++++++++++++-- .../HierarchicalWithEnumParentInputMapper.php | 11 +++++++++- .../Object/MapDiscriminatedObjectTest.php | 7 +++--- .../DefaultMapperCompilerFactoryTest.php | 4 ++-- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index 1913b67..14ebf34 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ShipMonk\InputMapper\Compiler\CompiledExpr; use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler; +use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable; use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder; @@ -28,7 +29,7 @@ class MapDiscriminatedObject implements GenericMapperCompiler /** * @param class-string $className - * @param array $subtypeCompilers + * @param array $subtypeCompilers * @param list $genericParameters */ public function __construct( @@ -42,8 +43,6 @@ public function __construct( public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr { - $provider = $builder->propertyFetch($builder->var('this'), 'provider'); - $statements = [ $builder->if($builder->not($builder->funcCall($builder->importFunction('is_array'), [$value])), [ $builder->throw( @@ -88,14 +87,16 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $subtypeMatchArms = []; - foreach ($this->subtypeCompilers as $key => $subtype) { - $mapperProviderMethodCall = $builder->methodCall($provider, 'get', [ - $builder->classConstFetch($builder->importClass($subtype), 'class'), - ]); + foreach ($this->subtypeCompilers as $key => $subtypeCompiler) { + $subtypeMapperMethodName = $builder->uniqMethodName('map' . ucfirst($key)); + $subtypeMapperMethod = $builder->mapperMethod($subtypeMapperMethodName, $subtypeCompiler)->makePrivate()->getNode(); + + $builder->addMethod($subtypeMapperMethod); + $subtypeMapperMethodCall = $builder->methodCall($builder->var('this'), $subtypeMapperMethodName, [$value, $path]); $subtypeMatchArms[] = $builder->matchArm( $builder->val($key), - $builder->methodCall($mapperProviderMethodCall, 'map', [$value, $path]), + $subtypeMapperMethodCall, ); } diff --git a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php index 326968b..2402c4c 100644 --- a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php +++ b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php @@ -65,6 +65,7 @@ use ShipMonk\InputMapper\Runtime\Optional; use function array_column; use function array_fill_keys; +use function array_map; use function class_exists; use function class_implements; use function class_parents; @@ -346,10 +347,15 @@ public function createDiscriminatorObjectMapping( $inputType = new IdentifierTypeNode($inputClassName); $genericParameters = PhpDocTypeUtils::getGenericTypeDefinition($inputType)->parameters; + $subtypeMappers = array_map( + static fn (string $subtypeClassName): MapperCompiler => new DelegateMapperCompiler($subtypeClassName), + $discriminatorAttribute->mapping, + ); + return new MapDiscriminatedObject( $inputClassName, $discriminatorAttribute->key, - $discriminatorAttribute->mapping, + $subtypeMappers, $genericParameters, ); } diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index 9419053..e1fb23c 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -37,8 +37,8 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput } return match ($this->mapType($data['type'], [...$path, 'type'])) { - 'childOne' => $this->provider->get(HierarchicalChildOneInput::class)->map($data, $path), - 'childTwo' => $this->provider->get(HierarchicalChildTwoInput::class)->map($data, $path), + 'childOne' => $this->mapChildOne($data, $path), + 'childTwo' => $this->mapChildTwo($data, $path), default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])), }; } @@ -61,4 +61,22 @@ private function mapType(mixed $data, array $path = []): ?string return $mapped; } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOne(mixed $data, array $path = []): HierarchicalChildOneInput + { + return $this->provider->get(HierarchicalChildOneInput::class)->map($data, $path); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildTwo(mixed $data, array $path = []): HierarchicalChildTwoInput + { + return $this->provider->get(HierarchicalChildTwoInput::class)->map($data, $path); + } } diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index 30ce0a0..987f5ab 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -37,7 +37,7 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn } return match ($this->mapType($data['type'], [...$path, 'type'])) { - 'childOne' => $this->provider->get(HierarchicalWithEnumChildInput::class)->map($data, $path), + 'childOne' => $this->mapChildOne($data, $path), default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])), }; } @@ -60,4 +60,13 @@ private function mapType(mixed $data, array $path = []): ?string return $mapped; } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOne(mixed $data, array $path = []): HierarchicalWithEnumChildInput + { + return $this->provider->get(HierarchicalWithEnumChildInput::class)->map($data, $path); + } } diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php index 95b2cd0..256ec84 100644 --- a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -3,6 +3,7 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object; use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; +use ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapEnum; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapObject; @@ -142,8 +143,8 @@ private function createParentInputMapperCompiler(): MapperCompiler HierarchicalParentInput::class, 'type', [ - 'childOne' => HierarchicalChildOneInput::class, - 'childTwo' => HierarchicalChildTwoInput::class, + 'childOne' => new DelegateMapperCompiler(HierarchicalChildOneInput::class), + 'childTwo' => new DelegateMapperCompiler(HierarchicalChildTwoInput::class), ], ); } @@ -154,7 +155,7 @@ private function createParentInputWithEnumMapperCompiler(): MapperCompiler HierarchicalWithEnumParentInput::class, 'type', [ - 'childOne' => HierarchicalWithEnumChildInput::class, + 'childOne' => new DelegateMapperCompiler(HierarchicalWithEnumChildInput::class), ], ); } diff --git a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php index 6d68275..f4dd337 100644 --- a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php +++ b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php @@ -183,8 +183,8 @@ className: CarFilterInput::class, className: AnimalInput::class, discriminatorFieldName: 'type', subtypeCompilers: [ - AnimalType::Cat->value => AnimalCatInput::class, - AnimalType::Dog->value => AnimalDogInput::class, + AnimalType::Cat->value => new DelegateMapperCompiler(AnimalCatInput::class), + AnimalType::Dog->value => new DelegateMapperCompiler(AnimalDogInput::class), ], ), ]; From 25ba92c8915c6dc865b56f338340d5f829f75a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 13:15:14 +0200 Subject: [PATCH 11/16] Add validation for subtypes during compilation --- .../CannotCompileMapperException.php | 19 +++++++++++++++++++ .../Mapper/Object/MapDiscriminatedObject.php | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Compiler/Exception/CannotCompileMapperException.php b/src/Compiler/Exception/CannotCompileMapperException.php index a91b6cb..36fad44 100644 --- a/src/Compiler/Exception/CannotCompileMapperException.php +++ b/src/Compiler/Exception/CannotCompileMapperException.php @@ -5,6 +5,7 @@ use LogicException; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; +use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler; use Throwable; @@ -24,6 +25,24 @@ public static function withIncompatibleMapper( return new self("Cannot compile mapper {$mapperCompilerClass}, because {$reason}", 0, $previous); } + /** + * @template T of object + * @param MapDiscriminatedObject $mapperCompiler + */ + public static function withIncompatibleSubtypeMapper( + MapDiscriminatedObject $mapperCompiler, + MapperCompiler $subtypeMapperCompiler, + ?Throwable $previous = null + ): self + { + $mapperOutputType = $mapperCompiler->getOutputType(); + $subtypeMapperCompilerClass = $subtypeMapperCompiler::class; + $subtypeMapperOutputType = $subtypeMapperCompiler->getOutputType(); + + $reason = "its output type '{$subtypeMapperOutputType}' is not super type of '{$mapperOutputType}'"; + return new self("Cannot compile mapper {$subtypeMapperCompilerClass} as subtype (#[Discriminator]) mapper, because {$reason}", 0, $previous); + } + public static function withIncompatibleValidator( ValidatorCompiler $validatorCompiler, MapperCompiler $mapperCompiler, diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index 14ebf34..ec9f5f9 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -9,12 +9,14 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ShipMonk\InputMapper\Compiler\CompiledExpr; +use ShipMonk\InputMapper\Compiler\Exception\CannotCompileMapperException; use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable; use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder; use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter; +use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; use function array_keys; use function count; @@ -43,6 +45,12 @@ public function __construct( public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr { + foreach ($this->subtypeCompilers as $subtypeCompiler) { + if (!PhpDocTypeUtils::isSubTypeOf($subtypeCompiler->getOutputType(), $this->getOutputType())) { + throw CannotCompileMapperException::withIncompatibleSubtypeMapper($this, $subtypeCompiler); + } + } + $statements = [ $builder->if($builder->not($builder->funcCall($builder->importFunction('is_array'), [$value])), [ $builder->throw( From 22de13c5577cd399a4f1ef49495329988f483e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 13:24:10 +0200 Subject: [PATCH 12/16] Add test for invalid hierarchy --- .../Object/MapDiscriminatedObjectTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php index 256ec84..cd6cf29 100644 --- a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object; +use ShipMonk\InputMapper\Compiler\Exception\CannotCompileMapperException; use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDiscriminatedObject; @@ -11,6 +12,7 @@ use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapOptional; use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException; +use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\Optional; use ShipMonkTests\InputMapper\Compiler\Mapper\MapperCompilerTestCase; use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalChildOneInput; @@ -19,6 +21,7 @@ use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithEnumChildInput; use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithEnumParentInput; use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithEnumType; +use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\MovieInput; class MapDiscriminatedObjectTest extends MapperCompilerTestCase { @@ -137,6 +140,24 @@ public function testCompileWithEnumAsType(): void ); } + public function testCompileWithSubtypesFromDifferentHierarchies(): void + { + $mapperCompiler = new MapDiscriminatedObject( + HierarchicalParentInput::class, + 'type', + [ + 'childOne' => new DelegateMapperCompiler(HierarchicalChildOneInput::class), + 'childTwo' => new DelegateMapperCompiler(MovieInput::class), + ], + ); + + self::assertException( + CannotCompileMapperException::class, + 'Cannot compile mapper ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler as subtype (#[Discriminator]) mapper, because its output type \'ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\MovieInput\' is not super type of \'ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalParentInput\'', + fn(): Mapper => $this->compileMapper('InvalidHierarchyMapper', $mapperCompiler), + ); + } + private function createParentInputMapperCompiler(): MapperCompiler { return new MapDiscriminatedObject( From 150bc5d16a2f93cfd72db290e6105bdfd2791784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 13:50:39 +0200 Subject: [PATCH 13/16] Update README, handle special chars in discriminator name --- README.md | 90 +++++++++++++++++++ src/Compiler/Mapper/Object/Discriminator.php | 5 +- .../Mapper/Object/MapDiscriminatedObject.php | 12 +-- .../Data/HierarchicalParentInputMapper.php | 4 +- .../HierarchicalWithEnumParentInputMapper.php | 4 +- .../HierarchicalWithNoTypeFieldChildInput.php | 16 ++++ ...HierarchicalWithNoTypeFieldInputMapper.php | 72 +++++++++++++++ ...rchicalWithNoTypeFieldChildInputMapper.php | 74 +++++++++++++++ ...HierarchicalWithNoTypeFieldParentInput.php | 22 +++++ .../Object/MapDiscriminatedObjectTest.php | 53 +++++++++++ 10 files changed, 342 insertions(+), 10 deletions(-) create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldChildInput.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInputMapper.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInput__HierarchicalWithNoTypeFieldChildInputMapper.php create mode 100644 tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldParentInput.php diff --git a/README.md b/README.md index 5c4667e..b5e2bf0 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,96 @@ class Person } ``` +### Parsing polymorphic classes (subtypes with a common parent) + +If you need to parse a hierarchy of classes, you can use the `#[Discriminator]` attribute. +(The discriminator field does not need to be mapped to a property if `#[AllowExtraKeys]` is used.) + +```php +use ShipMonk\InputMapper\Compiler\Mapper\Object\Discriminator; + +#[Discriminator( + key: 'type', // key to use for mapping + mapping: [ + 'car' => Car::class, + 'truck' => Truck::class, + ] +)] +abstract class Vehicle { + public function __construct( + public readonly string $type, + ) {} +} + +class Car extends Vehicle { + + public function __construct( + string $type, + public readonly string $color, + ) { + parent::__construct($type); + } + +} + +class Truck extends Vehicle { + + public function __construct( + string $type, + public readonly string $color, + ) { + parent::__construct($type); + } + +} +``` + +or, with enum: + +```php +use ShipMonk\InputMapper\Compiler\Mapper\Object\Discriminator; + +enum VehicleType: string { + case Car = 'car'; + case Truck = 'truck'; +} + +#[Discriminator( + key: 'type', // key to use for mapping + mapping: [ + VehicleType::Car->value => Car::class, + VehicleType::Truck->value => Truck::class, + ] +)] +abstract class Vehicle { + public function __construct( + VehicleType $type, + ) {} +} + +class Car extends Vehicle { + + public function __construct( + VehicleType $type, + public readonly string $color, + ) { + parent::__construct($type); + } + +} + +class Truck extends Vehicle { + + public function __construct( + VehicleType $type, + public readonly string $color, + ) { + parent::__construct($type); + } + +} +``` + ### Using custom mappers To map classes with your custom mapper, you need to implement `ShipMonk\InputMapper\Runtime\Mapper` interface and register it with `MapperProvider`: diff --git a/src/Compiler/Mapper/Object/Discriminator.php b/src/Compiler/Mapper/Object/Discriminator.php index bce4585..f0ab0b1 100644 --- a/src/Compiler/Mapper/Object/Discriminator.php +++ b/src/Compiler/Mapper/Object/Discriminator.php @@ -4,12 +4,15 @@ use Attribute; +/** + * Provides a way to map a polymorphic classes with common base class, according to the discriminator key. + */ #[Attribute(Attribute::TARGET_CLASS)] class Discriminator { /** - * @param array $mapping + * @param array $mapping Mapping of discriminator values to class names */ public function __construct( public readonly string $key, diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index ec9f5f9..cc71366 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -63,7 +63,9 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi ]), ]; - $isDiscriminatorPresent = $builder->funcCall($builder->importFunction('array_key_exists'), [$builder->val($this->discriminatorFieldName), $value]); + $discriminatorFieldNameValue = $builder->val($this->discriminatorFieldName); + + $isDiscriminatorPresent = $builder->funcCall($builder->importFunction('array_key_exists'), [$discriminatorFieldNameValue, $value]); $isDiscriminatorMissing = $builder->not($isDiscriminatorPresent); $statements[] = $builder->if($isDiscriminatorMissing, [ @@ -71,14 +73,14 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $builder->staticCall( $builder->importClass(MappingFailedException::class), 'missingKey', - [$path, $this->discriminatorFieldName], + [$path, $discriminatorFieldNameValue], ), ), ]); - $discriminatorRawValue = $builder->arrayDimFetch($value, $builder->val($this->discriminatorFieldName)); - $discriminatorPath = $builder->arrayImmutableAppend($path, $builder->val($this->discriminatorFieldName)); - $discriminatorMapperMethodName = $builder->uniqMethodName('map' . ucfirst($this->discriminatorFieldName)); + $discriminatorRawValue = $builder->arrayDimFetch($value, $discriminatorFieldNameValue); + $discriminatorPath = $builder->arrayImmutableAppend($path, $discriminatorFieldNameValue); + $discriminatorMapperMethodName = $builder->uniqMethodName('mapDiscriminatorField'); $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, new MapNullable(new MapString()))->makePrivate()->getNode(); $discriminatorMapperCall = $builder->methodCall($builder->var('this'), $discriminatorMapperMethodName, [$discriminatorRawValue, $discriminatorPath]); $builder->addMethod($discriminatorMapperMethod); diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index e1fb23c..8c417fd 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -36,7 +36,7 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput throw MappingFailedException::missingKey($path, 'type'); } - return match ($this->mapType($data['type'], [...$path, 'type'])) { + return match ($this->mapDiscriminatorField($data['type'], [...$path, 'type'])) { 'childOne' => $this->mapChildOne($data, $path), 'childTwo' => $this->mapChildTwo($data, $path), default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])), @@ -47,7 +47,7 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput * @param list $path * @throws MappingFailedException */ - private function mapType(mixed $data, array $path = []): ?string + private function mapDiscriminatorField(mixed $data, array $path = []): ?string { if ($data === null) { $mapped = null; diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index 987f5ab..8d70979 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -36,7 +36,7 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn throw MappingFailedException::missingKey($path, 'type'); } - return match ($this->mapType($data['type'], [...$path, 'type'])) { + return match ($this->mapDiscriminatorField($data['type'], [...$path, 'type'])) { 'childOne' => $this->mapChildOne($data, $path), default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])), }; @@ -46,7 +46,7 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn * @param list $path * @throws MappingFailedException */ - private function mapType(mixed $data, array $path = []): ?string + private function mapDiscriminatorField(mixed $data, array $path = []): ?string { if ($data === null) { $mapped = null; diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldChildInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldChildInput.php new file mode 100644 index 0000000..9cbf6c5 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldChildInput.php @@ -0,0 +1,16 @@ + + */ +class HierarchicalWithNoTypeFieldInputMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalWithNoTypeFieldParentInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('$type', $data)) { + throw MappingFailedException::missingKey($path, '$type'); + } + + return match ($this->mapDiscriminatorField($data['$type'], [...$path, '$type'])) { + 'childOne' => $this->mapChildOne($data, $path), + default => throw MappingFailedException::incorrectValue($data['$type'], [...$path, '$type'], 'one of ' . implode(', ', ['childOne'])), + }; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapDiscriminatorField(mixed $data, array $path = []): ?string + { + if ($data === null) { + $mapped = null; + } else { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $mapped = $data; + } + + return $mapped; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOne(mixed $data, array $path = []): HierarchicalWithNoTypeFieldChildInput + { + return $this->provider->get(HierarchicalWithNoTypeFieldChildInput::class)->map($data, $path); + } +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInput__HierarchicalWithNoTypeFieldChildInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInput__HierarchicalWithNoTypeFieldChildInputMapper.php new file mode 100644 index 0000000..0bd99d2 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInput__HierarchicalWithNoTypeFieldChildInputMapper.php @@ -0,0 +1,74 @@ + + */ +class HierarchicalWithNoTypeFieldInput__HierarchicalWithNoTypeFieldChildInputMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): HierarchicalWithNoTypeFieldChildInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('id', $data)) { + throw MappingFailedException::missingKey($path, 'id'); + } + + if (!array_key_exists('childOneField', $data)) { + throw MappingFailedException::missingKey($path, 'childOneField'); + } + + return new HierarchicalWithNoTypeFieldChildInput( + $this->mapId($data['id'], [...$path, 'id']), + $this->mapChildOneField($data['childOneField'], [...$path, 'childOneField']), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapId(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapChildOneField(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldParentInput.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldParentInput.php new file mode 100644 index 0000000..674bf02 --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldParentInput.php @@ -0,0 +1,22 @@ + HierarchicalWithNoTypeFieldChildInput::class, + ], +)] +abstract class HierarchicalWithNoTypeFieldParentInput +{ + + public function __construct( + public readonly int $id, + ) + { + } + +} diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php index cd6cf29..ccf6d80 100644 --- a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -21,6 +21,8 @@ use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithEnumChildInput; use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithEnumParentInput; use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithEnumType; +use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithNoTypeFieldChildInput; +use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalWithNoTypeFieldParentInput; use ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\MovieInput; class MapDiscriminatedObjectTest extends MapperCompilerTestCase @@ -140,6 +142,38 @@ public function testCompileWithEnumAsType(): void ); } + public function testCompileWithNoTypeFieldMapping(): void + { + $parentInputMapper = $this->compileMapper('HierarchicalWithNoTypeFieldInput', $this->createParentInputWithNoTypeFieldMapperCompiler(), [ + HierarchicalWithNoTypeFieldChildInput::class => $this->createHierarchicalChildWithNoTypeFieldMapperCompiler(), + ]); + + $childOneInputObject = new HierarchicalWithNoTypeFieldChildInput( + id: 1, + childOneField: 'abc', + ); + + $childOneInputArray = [ + 'id' => 1, + '$type' => 'childOne', + 'childOneField' => 'abc', + ]; + + self::assertEquals($childOneInputObject, $parentInputMapper->map($childOneInputArray)); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /$type: Expected one of childOne, got null', + static fn() => $parentInputMapper->map([...$childOneInputArray, '$type' => null]), + ); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /$type: Expected one of childOne, got "c"', + static fn() => $parentInputMapper->map([...$childOneInputArray, '$type' => 'c']), + ); + } + public function testCompileWithSubtypesFromDifferentHierarchies(): void { $mapperCompiler = new MapDiscriminatedObject( @@ -211,4 +245,23 @@ public function createHierarchicalChildWithEnumMapperCompiler(): MapperCompiler ]); } + private function createParentInputWithNoTypeFieldMapperCompiler(): MapperCompiler + { + return new MapDiscriminatedObject( + HierarchicalWithNoTypeFieldParentInput::class, + '$type', + [ + 'childOne' => new DelegateMapperCompiler(HierarchicalWithNoTypeFieldChildInput::class), + ], + ); + } + + public function createHierarchicalChildWithNoTypeFieldMapperCompiler(): MapperCompiler + { + return new MapObject(HierarchicalWithNoTypeFieldChildInput::class, [ + 'id' => new MapInt(), + 'childOneField' => new MapString(), + ], allowExtraKeys: true); + } + } From 30d819bacaf8040f37d70b0af780f985bf1adf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 15:23:40 +0200 Subject: [PATCH 14/16] Drop discriminator field mapper, use value directly --- .../Mapper/Object/MapDiscriminatedObject.php | 8 +------ .../Data/HierarchicalParentInputMapper.php | 22 +------------------ .../HierarchicalWithEnumParentInputMapper.php | 22 +------------------ ...HierarchicalWithNoTypeFieldInputMapper.php | 22 +------------------ 4 files changed, 4 insertions(+), 70 deletions(-) diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index cc71366..75e1778 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -12,8 +12,6 @@ use ShipMonk\InputMapper\Compiler\Exception\CannotCompileMapperException; use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; -use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; -use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable; use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder; use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter; use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils; @@ -80,10 +78,6 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $discriminatorRawValue = $builder->arrayDimFetch($value, $discriminatorFieldNameValue); $discriminatorPath = $builder->arrayImmutableAppend($path, $discriminatorFieldNameValue); - $discriminatorMapperMethodName = $builder->uniqMethodName('mapDiscriminatorField'); - $discriminatorMapperMethod = $builder->mapperMethod($discriminatorMapperMethodName, new MapNullable(new MapString()))->makePrivate()->getNode(); - $discriminatorMapperCall = $builder->methodCall($builder->var('this'), $discriminatorMapperMethodName, [$discriminatorRawValue, $discriminatorPath]); - $builder->addMethod($discriminatorMapperMethod); $validMappingKeys = array_keys($this->subtypeCompilers); @@ -121,7 +115,7 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi ), ); - $matchedSubtype = $builder->match($discriminatorMapperCall, $subtypeMatchArms); + $matchedSubtype = $builder->match($discriminatorRawValue, $subtypeMatchArms); return new CompiledExpr( $matchedSubtype, diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php index 8c417fd..523bb05 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalParentInputMapper.php @@ -9,7 +9,6 @@ use function array_key_exists; use function implode; use function is_array; -use function is_string; /** * Generated mapper by {@see MapDiscriminatedObject}. Do not edit directly. @@ -36,32 +35,13 @@ public function map(mixed $data, array $path = []): HierarchicalParentInput throw MappingFailedException::missingKey($path, 'type'); } - return match ($this->mapDiscriminatorField($data['type'], [...$path, 'type'])) { + return match ($data['type']) { 'childOne' => $this->mapChildOne($data, $path), 'childTwo' => $this->mapChildTwo($data, $path), default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne', 'childTwo'])), }; } - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapDiscriminatorField(mixed $data, array $path = []): ?string - { - if ($data === null) { - $mapped = null; - } else { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - $mapped = $data; - } - - return $mapped; - } - /** * @param list $path * @throws MappingFailedException diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php index 8d70979..d9041c4 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithEnumParentInputMapper.php @@ -9,7 +9,6 @@ use function array_key_exists; use function implode; use function is_array; -use function is_string; /** * Generated mapper by {@see MapDiscriminatedObject}. Do not edit directly. @@ -36,31 +35,12 @@ public function map(mixed $data, array $path = []): HierarchicalWithEnumParentIn throw MappingFailedException::missingKey($path, 'type'); } - return match ($this->mapDiscriminatorField($data['type'], [...$path, 'type'])) { + return match ($data['type']) { 'childOne' => $this->mapChildOne($data, $path), default => throw MappingFailedException::incorrectValue($data['type'], [...$path, 'type'], 'one of ' . implode(', ', ['childOne'])), }; } - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapDiscriminatorField(mixed $data, array $path = []): ?string - { - if ($data === null) { - $mapped = null; - } else { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - $mapped = $data; - } - - return $mapped; - } - /** * @param list $path * @throws MappingFailedException diff --git a/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInputMapper.php b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInputMapper.php index 077ece4..82f1c8d 100644 --- a/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/HierarchicalWithNoTypeFieldInputMapper.php @@ -9,7 +9,6 @@ use function array_key_exists; use function implode; use function is_array; -use function is_string; /** * Generated mapper by {@see MapDiscriminatedObject}. Do not edit directly. @@ -36,31 +35,12 @@ public function map(mixed $data, array $path = []): HierarchicalWithNoTypeFieldP throw MappingFailedException::missingKey($path, '$type'); } - return match ($this->mapDiscriminatorField($data['$type'], [...$path, '$type'])) { + return match ($data['$type']) { 'childOne' => $this->mapChildOne($data, $path), default => throw MappingFailedException::incorrectValue($data['$type'], [...$path, '$type'], 'one of ' . implode(', ', ['childOne'])), }; } - /** - * @param list $path - * @throws MappingFailedException - */ - private function mapDiscriminatorField(mixed $data, array $path = []): ?string - { - if ($data === null) { - $mapped = null; - } else { - if (!is_string($data)) { - throw MappingFailedException::incorrectType($data, $path, 'string'); - } - - $mapped = $data; - } - - return $mapped; - } - /** * @param list $path * @throws MappingFailedException From b255adf2cac2df479bfc5783cf43215e0fb17fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Mon, 9 Sep 2024 15:31:40 +0200 Subject: [PATCH 15/16] Rename discriminatorFieldName to discriminatorKey --- .../Mapper/Object/MapDiscriminatedObject.php | 12 ++++++------ .../DefaultMapperCompilerFactoryTest.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php index 75e1778..4e319ec 100644 --- a/src/Compiler/Mapper/Object/MapDiscriminatedObject.php +++ b/src/Compiler/Mapper/Object/MapDiscriminatedObject.php @@ -34,7 +34,7 @@ class MapDiscriminatedObject implements GenericMapperCompiler */ public function __construct( public readonly string $className, - public readonly string $discriminatorFieldName, + public readonly string $discriminatorKeyName, public readonly array $subtypeCompilers, public readonly array $genericParameters = [], ) @@ -61,9 +61,9 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi ]), ]; - $discriminatorFieldNameValue = $builder->val($this->discriminatorFieldName); + $discriminatorKeyAsValue = $builder->val($this->discriminatorKeyName); - $isDiscriminatorPresent = $builder->funcCall($builder->importFunction('array_key_exists'), [$discriminatorFieldNameValue, $value]); + $isDiscriminatorPresent = $builder->funcCall($builder->importFunction('array_key_exists'), [$discriminatorKeyAsValue, $value]); $isDiscriminatorMissing = $builder->not($isDiscriminatorPresent); $statements[] = $builder->if($isDiscriminatorMissing, [ @@ -71,13 +71,13 @@ public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): Compi $builder->staticCall( $builder->importClass(MappingFailedException::class), 'missingKey', - [$path, $discriminatorFieldNameValue], + [$path, $discriminatorKeyAsValue], ), ), ]); - $discriminatorRawValue = $builder->arrayDimFetch($value, $discriminatorFieldNameValue); - $discriminatorPath = $builder->arrayImmutableAppend($path, $discriminatorFieldNameValue); + $discriminatorRawValue = $builder->arrayDimFetch($value, $discriminatorKeyAsValue); + $discriminatorPath = $builder->arrayImmutableAppend($path, $discriminatorKeyAsValue); $validMappingKeys = array_keys($this->subtypeCompilers); diff --git a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php index f4dd337..2a441d9 100644 --- a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php +++ b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php @@ -181,7 +181,7 @@ className: CarFilterInput::class, [], new MapDiscriminatedObject( className: AnimalInput::class, - discriminatorFieldName: 'type', + discriminatorKeyName: 'type', subtypeCompilers: [ AnimalType::Cat->value => new DelegateMapperCompiler(AnimalCatInput::class), AnimalType::Dog->value => new DelegateMapperCompiler(AnimalDogInput::class), From 63426b6eeafa13f611b374c50bef8bea8bc6b7cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ol=C5=A1avsk=C3=BD?= Date: Mon, 9 Sep 2024 15:59:04 +0200 Subject: [PATCH 16/16] Update src/Compiler/Exception/CannotCompileMapperException.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Tvrdík --- src/Compiler/Exception/CannotCompileMapperException.php | 2 +- tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Exception/CannotCompileMapperException.php b/src/Compiler/Exception/CannotCompileMapperException.php index 36fad44..0192f93 100644 --- a/src/Compiler/Exception/CannotCompileMapperException.php +++ b/src/Compiler/Exception/CannotCompileMapperException.php @@ -39,7 +39,7 @@ public static function withIncompatibleSubtypeMapper( $subtypeMapperCompilerClass = $subtypeMapperCompiler::class; $subtypeMapperOutputType = $subtypeMapperCompiler->getOutputType(); - $reason = "its output type '{$subtypeMapperOutputType}' is not super type of '{$mapperOutputType}'"; + $reason = "its output type '{$subtypeMapperOutputType}' is not subtype of '{$mapperOutputType}'"; return new self("Cannot compile mapper {$subtypeMapperCompilerClass} as subtype (#[Discriminator]) mapper, because {$reason}", 0, $previous); } diff --git a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php index ccf6d80..d3e48ac 100644 --- a/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapDiscriminatedObjectTest.php @@ -187,7 +187,7 @@ public function testCompileWithSubtypesFromDifferentHierarchies(): void self::assertException( CannotCompileMapperException::class, - 'Cannot compile mapper ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler as subtype (#[Discriminator]) mapper, because its output type \'ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\MovieInput\' is not super type of \'ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalParentInput\'', + 'Cannot compile mapper ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler as subtype (#[Discriminator]) mapper, because its output type \'ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\MovieInput\' is not subtype of \'ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data\HierarchicalParentInput\'', fn(): Mapper => $this->compileMapper('InvalidHierarchyMapper', $mapperCompiler), ); }