From 13f69a9e1b5ce7f7a2305933764a928adf08c7df Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Mon, 1 Apr 2024 15:39:41 +0200 Subject: [PATCH] feat: handle interface constructor registration By default, the mapper cannot instantiate an interface, as it does not know which implementation to use. To do so, the `MapperBuilder::infer()` method can be used, but it is cumbersome in most cases. It is now also possible to register a constructor for an interface, in the same way as for a class. Because the mapper cannot automatically guess which implementation can be used for an interface, it is not possible to use the `Constructor` attribute, the `MapperBuilder::registerConstructor()` method must be used instead. In the example below, the mapper is taught how to instantiate an implementation of `UuidInterface` from package `ramsey/uuid`: ```php (new \CuyZ\Valinor\MapperBuilder()) ->registerConstructor( // The static method below has return type `UuidInterface`; // therefore, the mapper will build an instance of `Uuid` when // it needs to instantiate an implementation of `UuidInterface`. Ramsey\Uuid\Uuid::fromString(...) ) ->mapper() ->map( Ramsey\Uuid\UuidInterface::class, '663bafbf-c3b5-4336-b27f-1796be8554e0' ); ``` --- docs/pages/how-to/infer-interfaces.md | 8 ++- .../how-to/use-custom-object-constructors.md | 31 +++++++++ src/Definition/ClassDefinition.php | 4 +- .../Exception/InvalidTypeAliasImportClass.php | 4 +- .../InvalidTypeAliasImportClassType.php | 4 +- .../Exception/UnknownTypeAliasImport.php | 4 +- .../Cache/CacheClassDefinitionRepository.php | 4 +- .../Repository/ClassDefinitionRepository.php | 4 +- .../ReflectionClassDefinitionRepository.php | 13 ++-- src/Library/Container.php | 16 +++-- .../ConstructorObjectBuilderFactory.php | 2 +- .../Factory/DateTimeObjectBuilderFactory.php | 4 +- .../DateTimeZoneObjectBuilderFactory.php | 4 +- src/Mapper/Object/FunctionObjectBuilder.php | 4 +- ...lder.php => FilteredObjectNodeBuilder.php} | 2 +- .../Tree/Builder/InterfaceNodeBuilder.php | 27 +++++++- .../Tree/Builder/NativeClassNodeBuilder.php | 9 +-- .../InterfaceHasBothConstructorAndInfer.php | 22 ++++++ src/Type/Types/InterfaceType.php | 2 +- .../FakeClassDefinitionRepository.php | 4 +- .../ConstructorRegistrationMappingTest.php | 67 +++++++++++++++++++ tests/Unit/Type/Types/InterfaceTypeTest.php | 16 +++-- 22 files changed, 206 insertions(+), 49 deletions(-) rename src/Mapper/Tree/Builder/{ObjectNodeBuilder.php => FilteredObjectNodeBuilder.php} (98%) create mode 100644 src/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php diff --git a/docs/pages/how-to/infer-interfaces.md b/docs/pages/how-to/infer-interfaces.md index 1d05a2f7..08207314 100644 --- a/docs/pages/how-to/infer-interfaces.md +++ b/docs/pages/how-to/infer-interfaces.md @@ -1,8 +1,10 @@ # Inferring interfaces When the mapper meets an interface, it needs to understand which implementation -(a class that implements this interface) will be used — this information must be -provided in the mapper builder, using the method `infer()`. +will be used. This can be done by [registering a constructor for the interface], +but it can have limitations. A more powerful way to handle this is to infer the +implementation based on the data provided to the mapper. This can be done using +the `infer()` method. The callback given to this method must return the name of a class that implements the interface. Any arguments can be required by the callback; they @@ -124,3 +126,5 @@ assert($result->foo === 'foo'); assert($result->bar === 'bar'); assert($result->baz === 'baz'); ``` + +[registering a constructor for the interface]: use-custom-object-constructors.md#interface-implementation-constructor diff --git a/docs/pages/how-to/use-custom-object-constructors.md b/docs/pages/how-to/use-custom-object-constructors.md index 55dc94f3..921f2a73 100644 --- a/docs/pages/how-to/use-custom-object-constructors.md +++ b/docs/pages/how-to/use-custom-object-constructors.md @@ -130,6 +130,37 @@ final class Color } ``` +## Interface implementation constructor + +By default, the mapper cannot instantiate an interface, as it does not know +which implementation to use. To do so, it is possible to register a constructor +for an interface, in the same way as for a class. + +!!! note + + Because the mapper cannot automatically guess which implementation can be + used for an interface, it is not possible to use the `Constructor` + attribute, the `MapperBuilder::registerConstructor()` method must be used + instead. + +In the example below, the mapper is taught how to instantiate an implementation +of `UuidInterface` from package [`ramsey/uuid`](https://github.com/ramsey/uuid): + +```php +(new \CuyZ\Valinor\MapperBuilder()) + ->registerConstructor( + // The static method below has return type `UuidInterface`; therefore, + // the mapper will build an instance of `Uuid` when it needs to + // instantiate an implementation of `UuidInterface`. + Ramsey\Uuid\Uuid::fromString(...) + ) + ->mapper() + ->map( + Ramsey\Uuid\UuidInterface::class, + '663bafbf-c3b5-4336-b27f-1796be8554e0' + ); +``` + ## Custom enum constructor Registering a constructor for an enum works the same way as for a class, as diff --git a/src/Definition/ClassDefinition.php b/src/Definition/ClassDefinition.php index 8d821cca..e3bd41ce 100644 --- a/src/Definition/ClassDefinition.php +++ b/src/Definition/ClassDefinition.php @@ -4,7 +4,7 @@ namespace CuyZ\Valinor\Definition; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; /** @internal */ final class ClassDefinition @@ -12,7 +12,7 @@ final class ClassDefinition public function __construct( /** @var class-string */ public readonly string $name, - public readonly ClassType $type, + public readonly ObjectType $type, public readonly Attributes $attributes, public readonly Properties $properties, public readonly Methods $methods, diff --git a/src/Definition/Exception/InvalidTypeAliasImportClass.php b/src/Definition/Exception/InvalidTypeAliasImportClass.php index 6175086e..8831bce2 100644 --- a/src/Definition/Exception/InvalidTypeAliasImportClass.php +++ b/src/Definition/Exception/InvalidTypeAliasImportClass.php @@ -4,13 +4,13 @@ namespace CuyZ\Valinor\Definition\Exception; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use LogicException; /** @internal */ final class InvalidTypeAliasImportClass extends LogicException { - public function __construct(ClassType $type, string $className) + public function __construct(ObjectType $type, string $className) { parent::__construct( "Cannot import a type alias from unknown class `$className` in class `{$type->className()}`.", diff --git a/src/Definition/Exception/InvalidTypeAliasImportClassType.php b/src/Definition/Exception/InvalidTypeAliasImportClassType.php index e2684323..f5f91cca 100644 --- a/src/Definition/Exception/InvalidTypeAliasImportClassType.php +++ b/src/Definition/Exception/InvalidTypeAliasImportClassType.php @@ -4,14 +4,14 @@ namespace CuyZ\Valinor\Definition\Exception; +use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Type; -use CuyZ\Valinor\Type\ClassType; use LogicException; /** @internal */ final class InvalidTypeAliasImportClassType extends LogicException { - public function __construct(ClassType $classType, Type $type) + public function __construct(ObjectType $classType, Type $type) { parent::__construct( "Importing a type alias can only be done with classes, `{$type->toString()}` was given in class `{$classType->className()}`.", diff --git a/src/Definition/Exception/UnknownTypeAliasImport.php b/src/Definition/Exception/UnknownTypeAliasImport.php index 5f304b1d..def92da0 100644 --- a/src/Definition/Exception/UnknownTypeAliasImport.php +++ b/src/Definition/Exception/UnknownTypeAliasImport.php @@ -4,7 +4,7 @@ namespace CuyZ\Valinor\Definition\Exception; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use LogicException; /** @internal */ @@ -13,7 +13,7 @@ final class UnknownTypeAliasImport extends LogicException /** * @param class-string $importClassName */ - public function __construct(ClassType $type, string $importClassName, string $alias) + public function __construct(ObjectType $type, string $importClassName, string $alias) { parent::__construct( "Type alias `$alias` imported in `{$type->className()}` could not be found in `$importClassName`", diff --git a/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php b/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php index e8c1ff0a..c23cdfa8 100644 --- a/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php +++ b/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php @@ -6,7 +6,7 @@ use CuyZ\Valinor\Definition\ClassDefinition; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use Psr\SimpleCache\CacheInterface; use function sha1; @@ -20,7 +20,7 @@ public function __construct( private CacheInterface $cache ) {} - public function for(ClassType $type): ClassDefinition + public function for(ObjectType $type): ClassDefinition { // @infection-ignore-all $key = 'class-definition' . sha1($type->toString()); diff --git a/src/Definition/Repository/ClassDefinitionRepository.php b/src/Definition/Repository/ClassDefinitionRepository.php index 59517cb8..7d742843 100644 --- a/src/Definition/Repository/ClassDefinitionRepository.php +++ b/src/Definition/Repository/ClassDefinitionRepository.php @@ -5,10 +5,10 @@ namespace CuyZ\Valinor\Definition\Repository; use CuyZ\Valinor\Definition\ClassDefinition; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; /** @internal */ interface ClassDefinitionRepository { - public function for(ClassType $type): ClassDefinition; + public function for(ObjectType $type): ClassDefinition; } diff --git a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php index e1b4ec0c..1d034aba 100644 --- a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php @@ -21,7 +21,6 @@ use CuyZ\Valinor\Definition\PropertyDefinition; use CuyZ\Valinor\Definition\Repository\AttributesRepository; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; -use CuyZ\Valinor\Type\ClassType; use CuyZ\Valinor\Type\GenericType; use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; @@ -67,7 +66,7 @@ public function __construct(TypeParserFactory $typeParserFactory) $this->methodBuilder = new ReflectionMethodDefinitionBuilder($this->attributesRepository); } - public function for(ClassType $type): ClassDefinition + public function for(ObjectType $type): ClassDefinition { $reflection = Reflection::class($type->className()); @@ -97,7 +96,7 @@ private function attributes(ReflectionClass $reflection): array /** * @return list */ - private function properties(ClassType $type): array + private function properties(ObjectType $type): array { return array_map( function (ReflectionProperty $property) use ($type) { @@ -112,7 +111,7 @@ function (ReflectionProperty $property) use ($type) { /** * @return list */ - private function methods(ClassType $type): array + private function methods(ObjectType $type): array { $reflection = Reflection::class($type->className()); $methods = $reflection->getMethods(); @@ -134,7 +133,7 @@ private function methods(ClassType $type): array /** * @param ReflectionClass $target */ - private function typeResolver(ClassType $type, ReflectionClass $target): ReflectionTypeResolver + private function typeResolver(ObjectType $type, ReflectionClass $target): ReflectionTypeResolver { $typeKey = $target->isInterface() ? "{$type->toString()}/{$type->className()}" @@ -209,7 +208,7 @@ private function localTypeAliases(ObjectType $type): array /** * @return array */ - private function importedTypeAliases(ClassType $type): array + private function importedTypeAliases(ObjectType $type): array { $reflection = Reflection::class($type->className()); $importedTypesRaw = DocParser::importedTypeAliases($reflection); @@ -258,7 +257,7 @@ private function typeParser(ObjectType $type): TypeParser return $this->typeParserFactory->get(...$specs); } - private function parentType(ClassType $type): NativeClassType + private function parentType(ObjectType $type): NativeClassType { $reflection = Reflection::class($type->className()); diff --git a/src/Library/Container.php b/src/Library/Container.php index 85c66772..8f56574d 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -37,7 +37,7 @@ use CuyZ\Valinor\Mapper\Tree\Builder\NodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\NullNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\ObjectImplementations; -use CuyZ\Valinor\Mapper\Tree\Builder\ObjectNodeBuilder; +use CuyZ\Valinor\Mapper\Tree\Builder\FilteredObjectNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\ScalarNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\ShapedArrayNodeBuilder; @@ -54,7 +54,7 @@ use CuyZ\Valinor\Normalizer\Transformer\KeyTransformersHandler; use CuyZ\Valinor\Normalizer\Transformer\RecursiveTransformer; use CuyZ\Valinor\Normalizer\Transformer\ValueTransformersHandler; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\Factory\LexingTypeParserFactory; use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; use CuyZ\Valinor\Type\Parser\TypeParser; @@ -111,10 +111,10 @@ public function __construct(Settings $settings) ShapedArrayType::class => new ShapedArrayNodeBuilder($settings->allowSuperfluousKeys), ScalarType::class => new ScalarNodeBuilder($settings->enableFlexibleCasting), NullType::class => new NullNodeBuilder(), - ClassType::class => new NativeClassNodeBuilder( + ObjectType::class => new NativeClassNodeBuilder( $this->get(ClassDefinitionRepository::class), $this->get(ObjectBuilderFactory::class), - $this->get(ObjectNodeBuilder::class), + $this->get(FilteredObjectNodeBuilder::class), $settings->enableFlexibleCasting, ), ]); @@ -126,7 +126,11 @@ public function __construct(Settings $settings) $this->get(ObjectImplementations::class), $this->get(ClassDefinitionRepository::class), $this->get(ObjectBuilderFactory::class), - $this->get(ObjectNodeBuilder::class), + $this->get(FilteredObjectNodeBuilder::class), + new FunctionsContainer( + $this->get(FunctionDefinitionRepository::class), + $settings->customConstructors + ), $settings->enableFlexibleCasting, $settings->allowSuperfluousKeys, ); @@ -149,7 +153,7 @@ public function __construct(Settings $settings) return new ErrorCatcherNodeBuilder($builder, $settings->exceptionFilter); }, - ObjectNodeBuilder::class => fn () => new ObjectNodeBuilder($settings->allowSuperfluousKeys), + FilteredObjectNodeBuilder::class => fn () => new FilteredObjectNodeBuilder($settings->allowSuperfluousKeys), ObjectImplementations::class => fn () => new ObjectImplementations( new FunctionsContainer( diff --git a/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php b/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php index 4c6bed8a..22346f32 100644 --- a/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php @@ -131,7 +131,7 @@ private function builders(ClassDefinition $class): array return array_values($builders); } - private function constructorMatches(FunctionObject $function, ClassType $classType): bool + private function constructorMatches(FunctionObject $function, ObjectType $classType): bool { $definition = $function->definition; diff --git a/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php b/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php index a3a54fe5..fa5613ac 100644 --- a/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php @@ -12,7 +12,7 @@ use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder; use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder; use CuyZ\Valinor\Mapper\Object\ObjectBuilder; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use DateTime; use DateTimeImmutable; @@ -51,7 +51,7 @@ public function for(ClassDefinition $class): array return $builders; } - private function internalDateTimeBuilder(ClassType $type): FunctionObjectBuilder + private function internalDateTimeBuilder(ObjectType $type): FunctionObjectBuilder { $constructor = new DateTimeFormatConstructor(...$this->supportedDateFormats); $function = new FunctionObject($this->functionDefinitionRepository->for($constructor), $constructor); diff --git a/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php b/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php index 7f76a431..cc17713f 100644 --- a/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php @@ -11,7 +11,7 @@ use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder; use CuyZ\Valinor\Mapper\Object\ObjectBuilder; use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use DateTimeZone; use Exception; @@ -60,7 +60,7 @@ public function for(ClassDefinition $class): array return $builders; } - private function defaultBuilder(ClassType $type): FunctionObjectBuilder + private function defaultBuilder(ObjectType $type): FunctionObjectBuilder { $constructor = function (string $timezone) { try { diff --git a/src/Mapper/Object/FunctionObjectBuilder.php b/src/Mapper/Object/FunctionObjectBuilder.php index 281f3e7e..db1cc19d 100644 --- a/src/Mapper/Object/FunctionObjectBuilder.php +++ b/src/Mapper/Object/FunctionObjectBuilder.php @@ -7,7 +7,7 @@ use CuyZ\Valinor\Definition\FunctionObject; use CuyZ\Valinor\Definition\ParameterDefinition; use CuyZ\Valinor\Mapper\Tree\Message\UserlandError; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; use Exception; use function array_map; @@ -24,7 +24,7 @@ final class FunctionObjectBuilder implements ObjectBuilder private bool $isDynamicConstructor; - public function __construct(FunctionObject $function, ClassType $type) + public function __construct(FunctionObject $function, ObjectType $type) { $definition = $function->definition; diff --git a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php b/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php similarity index 98% rename from src/Mapper/Tree/Builder/ObjectNodeBuilder.php rename to src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php index d060de97..712de130 100644 --- a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php +++ b/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php @@ -12,7 +12,7 @@ use function count; /** @internal */ -final class ObjectNodeBuilder +final class FilteredObjectNodeBuilder { public function __construct(private bool $allowSuperfluousKeys) {} diff --git a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php index 36afb2a5..b96b663a 100644 --- a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php +++ b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder; +use CuyZ\Valinor\Definition\FunctionsContainer; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; use CuyZ\Valinor\Mapper\Object\Arguments; use CuyZ\Valinor\Mapper\Object\ArgumentsValues; @@ -11,9 +12,11 @@ use CuyZ\Valinor\Mapper\Object\FilteredObjectBuilder; use CuyZ\Valinor\Mapper\Tree\Exception\CannotInferFinalClass; use CuyZ\Valinor\Mapper\Tree\Exception\CannotResolveObjectType; +use CuyZ\Valinor\Mapper\Tree\Exception\InterfaceHasBothConstructorAndInfer; use CuyZ\Valinor\Mapper\Tree\Exception\ObjectImplementationCallbackError; use CuyZ\Valinor\Mapper\Tree\Message\UserlandError; use CuyZ\Valinor\Mapper\Tree\Shell; +use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\NativeClassType; use CuyZ\Valinor\Type\Types\InterfaceType; @@ -25,7 +28,8 @@ public function __construct( private ObjectImplementations $implementations, private ClassDefinitionRepository $classDefinitionRepository, private ObjectBuilderFactory $objectBuilderFactory, - private ObjectNodeBuilder $objectNodeBuilder, + private FilteredObjectNodeBuilder $filteredObjectNodeBuilder, + private FunctionsContainer $constructors, private bool $enableFlexibleCasting, private bool $allowSuperfluousKeys, ) {} @@ -38,6 +42,14 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode return $this->delegate->build($shell, $rootBuilder); } + if ($this->constructorRegisteredFor($type)) { + if ($this->implementations->has($type->className())) { + throw new InterfaceHasBothConstructorAndInfer($type->className()); + } + + return $this->delegate->build($shell, $rootBuilder); + } + if ($this->enableFlexibleCasting && $shell->value() === null) { $shell = $shell->withValue([]); } @@ -82,7 +94,18 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode $shell = $this->transformSourceForClass($shell, $arguments, $objectBuilder->describeArguments()); - return $this->objectNodeBuilder->build($objectBuilder, $shell, $rootBuilder); + return $this->filteredObjectNodeBuilder->build($objectBuilder, $shell, $rootBuilder); + } + + private function constructorRegisteredFor(Type $type): bool + { + foreach ($this->constructors as $constructor) { + if ($type->matches($constructor->definition->returnType)) { + return true; + } + } + + return false; } private function transformSourceForClass(Shell $shell, Arguments $interfaceArguments, Arguments $classArguments): Shell diff --git a/src/Mapper/Tree/Builder/NativeClassNodeBuilder.php b/src/Mapper/Tree/Builder/NativeClassNodeBuilder.php index f6b2d769..a487b5da 100644 --- a/src/Mapper/Tree/Builder/NativeClassNodeBuilder.php +++ b/src/Mapper/Tree/Builder/NativeClassNodeBuilder.php @@ -8,7 +8,8 @@ use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\FilteredObjectBuilder; use CuyZ\Valinor\Mapper\Tree\Shell; -use CuyZ\Valinor\Type\ClassType; + +use CuyZ\Valinor\Type\ObjectType; use function assert; @@ -18,7 +19,7 @@ final class NativeClassNodeBuilder implements NodeBuilder public function __construct( private ClassDefinitionRepository $classDefinitionRepository, private ObjectBuilderFactory $objectBuilderFactory, - private ObjectNodeBuilder $objectNodeBuilder, + private FilteredObjectNodeBuilder $filteredObjectNodeBuilder, private bool $enableFlexibleCasting, ) {} @@ -27,7 +28,7 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode $type = $shell->type(); // @infection-ignore-all - assert($type instanceof ClassType); + assert($type instanceof ObjectType); if ($this->enableFlexibleCasting && $shell->value() === null) { $shell = $shell->withValue([]); @@ -36,6 +37,6 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode $class = $this->classDefinitionRepository->for($type); $objectBuilder = FilteredObjectBuilder::from($shell->value(), ...$this->objectBuilderFactory->for($class)); - return $this->objectNodeBuilder->build($objectBuilder, $shell, $rootBuilder); + return $this->filteredObjectNodeBuilder->build($objectBuilder, $shell, $rootBuilder); } } diff --git a/src/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php b/src/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php new file mode 100644 index 00000000..77cb9851 --- /dev/null +++ b/src/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php @@ -0,0 +1,22 @@ +className(), $this->interfaceName, true); + return is_a($this->interfaceName, $other->className(), true); } public function traverse(): array diff --git a/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php b/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php index 015f5dca..727e45f2 100644 --- a/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php +++ b/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php @@ -7,11 +7,11 @@ use CuyZ\Valinor\Definition\ClassDefinition; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\ObjectType; final class FakeClassDefinitionRepository implements ClassDefinitionRepository { - public function for(ClassType $type): ClassDefinition + public function for(ObjectType $type): ClassDefinition { return FakeClassDefinition::new(); } diff --git a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php index 893c502f..6d1b1b1a 100644 --- a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php +++ b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php @@ -12,6 +12,7 @@ use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorReturnType; use CuyZ\Valinor\Mapper\Object\Exception\MissingConstructorClassTypeParameter; use CuyZ\Valinor\Mapper\Object\Exception\ObjectBuildersCollision; +use CuyZ\Valinor\Mapper\Tree\Exception\InterfaceHasBothConstructorAndInfer; use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage; use CuyZ\Valinor\Tests\Integration\IntegrationTestCase; use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\SimpleObject; @@ -678,6 +679,54 @@ public function test_registered_constructor_throwing_exception_fails_mapping_wit self::assertSame('some error message', (string)$error); } } + + public function test_two_registered_constructors_for_interface_work_properly(): void + { + $mapper = $this + ->mapperBuilder() + ->registerConstructor( + fn (string $foo, int $bar): SomeInterfaceWithRegisteredConstructor => new SomeClassImplementingInterfaceWithRegisteredConstructor($foo, $bar), + fn (string $bar, int $baz): SomeInterfaceWithRegisteredConstructor => new SomeOtherClassImplementingInterfaceWithRegisteredConstructor($bar, $baz), + ) + ->mapper(); + + try { + $resultA = $mapper->map(SomeInterfaceWithRegisteredConstructor::class, [ + 'foo' => 'foo', + 'bar' => 42, + ]); + + $resultB = $mapper->map(SomeInterfaceWithRegisteredConstructor::class, [ + 'bar' => 'bar', + 'baz' => 1337, + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertInstanceOf(SomeClassImplementingInterfaceWithRegisteredConstructor::class, $resultA); + self::assertSame('foo', $resultA->foo); + self::assertSame(42, $resultA->bar); + + self::assertInstanceOf(SomeOtherClassImplementingInterfaceWithRegisteredConstructor::class, $resultB); + self::assertSame('bar', $resultB->bar); + self::assertSame(1337, $resultB->baz); + } + + public function test_interface_with_both_constructor_and_infer_configurations_throws_exception(): void + { + $this->expectException(InterfaceHasBothConstructorAndInfer::class); + $this->expectExceptionCode(1711915749); + $this->expectExceptionMessage('Interface `' . SomeInterfaceWithRegisteredConstructor::class . '` is configured with at least one constructor but also has an infer configuration. Only one method can be used.'); + + $this->mapperBuilder() + ->registerConstructor( + fn (): SomeInterfaceWithRegisteredConstructor => new SomeClassImplementingInterfaceWithRegisteredConstructor('foo', 42) + ) + ->infer(SomeInterfaceWithRegisteredConstructor::class, fn () => SomeClassImplementingInterfaceWithRegisteredConstructor::class) + ->mapper() + ->map(SomeInterfaceWithRegisteredConstructor::class, []); + } } final class SomeClassWithNamedConstructors @@ -785,3 +834,21 @@ public static function getConstructor(stdClass $object): Closure return fn (): stdClass => $object; } } + +interface SomeInterfaceWithRegisteredConstructor {} + +final class SomeClassImplementingInterfaceWithRegisteredConstructor implements SomeInterfaceWithRegisteredConstructor +{ + public function __construct( + public string $foo, + public int $bar + ) {} +} + +final class SomeOtherClassImplementingInterfaceWithRegisteredConstructor implements SomeInterfaceWithRegisteredConstructor +{ + public function __construct( + public string $bar, + public int $baz + ) {} +} diff --git a/tests/Unit/Type/Types/InterfaceTypeTest.php b/tests/Unit/Type/Types/InterfaceTypeTest.php index 597fd44f..9f2837dd 100644 --- a/tests/Unit/Type/Types/InterfaceTypeTest.php +++ b/tests/Unit/Type/Types/InterfaceTypeTest.php @@ -66,8 +66,8 @@ public function test_matches_other_identical_interface(): void public function test_matches_sub_class(): void { - $interfaceTypeA = new InterfaceType(DateTimeInterface::class); - $interfaceTypeB = new InterfaceType(DateTime::class); + $interfaceTypeA = new InterfaceType(SomeChildInterface::class); + $interfaceTypeB = new InterfaceType(SomeParentInterface::class); self::assertTrue($interfaceTypeA->matches($interfaceTypeB)); } @@ -117,11 +117,11 @@ public function test_does_not_match_union_containing_invalid_type(): void public function test_matches_intersection_of_valid_types(): void { $intersectionType = new IntersectionType( - new InterfaceType(DateTimeInterface::class), - new InterfaceType(DateTime::class) + new InterfaceType(SomeParentInterface::class), + new InterfaceType(SomeOtherParentInterface::class), ); - self::assertTrue((new InterfaceType(DateTimeInterface::class))->matches($intersectionType)); + self::assertTrue((new InterfaceType(SomeChildInterface::class))->matches($intersectionType)); } public function test_does_not_match_intersection_containing_invalid_type(): void @@ -134,3 +134,9 @@ public function test_does_not_match_intersection_containing_invalid_type(): void self::assertFalse((new InterfaceType(DateTime::class))->matches($intersectionType)); } } + +interface SomeParentInterface {} + +interface SomeOtherParentInterface {} + +interface SomeChildInterface extends SomeParentInterface, SomeOtherParentInterface {}