Repository: CuyZ/Valinor (php)

Issue:
Inferring interfaces without knowing their implementation
Hi there 👋,

I'm not sure the title makes sense, but I hope it becomes clearer when I describe what I'm trying to do and how in order to learn if I've overlooked something in the docs or if it's something that's not possible.

An API that I'm writing an API client for provides URLs as strings, which I would like to map to a `\Psr\Http\Message\UriInterface` implementation in my objects:

```php
class UriWrapper
{
    public function __construct(
        public readonly UriInterface $uri
    ) {}
}
```

I'm using [php-http/discovery](https://github.com/php-http/discovery) to discover PSR-17 Factories. Depending on the installed PSR-7 implementation (e.g. `laminas/laminas-diactoros` or `guzzlehttp/psr7`), the `UriInterface` implementations differ.

If I knew which library to use, building the mapper would be fairly easy:

```php
<?php

use CuyZ\Valinor\MapperBuilder;
use Psr\Http\Message\UriInterface;

use Laminas\Diactoros\Uri;
// 👆 or 👇
// use GuzzleHttp\Psr7\Uri;

(new MapperBuilder())
    ->infer(UriInterface::class, fn() => Uri::class)
    ->mapper()
    ->map(
        UriWrapper::class,
        [
            'uri' => 'https://domain.tld'
        ]
    );
```

I could make it easy for myself and just ship an implementation with the API Client, but I would really prefer not to because of my almost clean `composer.json` 😅 


```json
{
    "require": {
        "php": "^8.1",
        "cuyz/valinor": "^0.14.0",
        "php-http/client-common": "^2.5",
        "php-http/discovery": "^1.14.3",
        "psr/http-client": "^1.0.1",
        "psr/http-client-implementation": "*",
        "psr/http-factory": "^1.0.1"
    }
}
```

I tried around and the closest I got was something like this:

```php
// Also a different factory depending on the installed implementation
$uriFactory = \Http\Discovery\Psr17FactoryDiscovery::findUriFactory();

(new MapperBuilder())
    ->registerConstructor(
        #[DynamicConstructor]
        function (string $className, string $value) use ($uriFactory): UriInterface {
            return $uriFactory->createUri($value);
        }
    )
    ->mapper()
    ->map(
        UriWrapper::class,
        [
            'uri' => 'https://domain.tld'
        ]
    );
```

which, as expected (but hoped otherwise) causes an exception:

```
Uncaught CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectType: Impossible to resolve an implementation for `Psr\Http\Message\UriInterface`
```

and now I'm out of ideas 😅 

I hope this makes sense and is preferably something I just didn't find or understand from the docs 🤞 - anyway I would really appreciate your help 🙏 
Registering ramseys `UuidInterface` seems complex
Hey there,

one thing I am still missing and unable to understand why it seems to be that complex:
Creating interface instances based on factory methods.

I do have objects implementing `UuidInterface` (from `ramsey/uuid`).
The base implementation for `Uuid` or for concrete versions do have a `__construct` not consuming `string`.


So lets say I do have:

```php
final class Foo
{
     public UuidInterface $uuid;
}
```

and a JSON source like: `{"uuid":"be3e4a03-1668-43c1-806a-f2903b54c9e0"}`

To have this working as expected, I do have to register a constructor **and** have to `infer` the interface like this:

```php
use Ramsey\Uuid\UuidInterface;
use Ramsey\Uuid\Uuid;

$this->mapperBuilder
    ->infer(UuidInterface::class, fn (string $value): string => Uuid::class)
    ->registerConstructor(fn (string $value): UuidInterface => Uuid::fromString($value));
```

---

Why is the `infer` part necessary? Is that, because `UuidInterface` returned from the callable is not a concrete class? Why would that be necessary? I'd expect that the constructor takes over and therefore I would not expect valinor to really care about the implementation.

Can you probably elaborate or do I just use this whole stuff wrong?
Thanks 



Pull request:
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'
    );
```

Fixes #217
Fixes #426 

Patch:
diff --git a/docs/pages/how-to/infer-interfaces.md b/docs/pages/how-to/infer-interfaces.md
index 1d05a2f..0820731 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 55dc94f..921f2a7 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/infection.json.dist b/infection.json.dist
index 75ba3b5..61ed9bd 100644
--- a/infection.json.dist
+++ b/infection.json.dist
@@ -1,5 +1,4 @@
 {
-    "timeout": 1,
     "source": {
         "directories": [
             "src"
diff --git a/src/Definition/ClassDefinition.php b/src/Definition/ClassDefinition.php
index 8d821cc..e3bd41c 100644
--- a/src/Definition/ClassDefinition.php
+++ b/src/Definition/ClassDefinition.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 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 6175086..8831bce 100644
--- a/src/Definition/Exception/InvalidTypeAliasImportClass.php
+++ b/src/Definition/Exception/InvalidTypeAliasImportClass.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 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 e268432..f5f91cc 100644
--- a/src/Definition/Exception/InvalidTypeAliasImportClassType.php
+++ b/src/Definition/Exception/InvalidTypeAliasImportClassType.php
@@ -4,14 +4,14 @@ declare(strict_types=1);
 
 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 5f304b1..def92da 100644
--- a/src/Definition/Exception/UnknownTypeAliasImport.php
+++ b/src/Definition/Exception/UnknownTypeAliasImport.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 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 e8c1ff0..c23cdfa 100644
--- a/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php
+++ b/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php
@@ -6,7 +6,7 @@ namespace CuyZ\Valinor\Definition\Repository\Cache;
 
 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 @@ final class CacheClassDefinitionRepository implements ClassDefinitionRepository
         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 59517cb..7d74284 100644
--- a/src/Definition/Repository/ClassDefinitionRepository.php
+++ b/src/Definition/Repository/ClassDefinitionRepository.php
@@ -5,10 +5,10 @@ declare(strict_types=1);
 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 e1b4ec0..1d034ab 100644
--- a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php
+++ b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php
@@ -21,7 +21,6 @@ use CuyZ\Valinor\Definition\Properties;
 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 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
         $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 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
     /**
      * @return list<PropertyDefinition>
      */
-    private function properties(ClassType $type): array
+    private function properties(ObjectType $type): array
     {
         return array_map(
             function (ReflectionProperty $property) use ($type) {
@@ -112,7 +111,7 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
     /**
      * @return list<MethodDefinition>
      */
-    private function methods(ClassType $type): array
+    private function methods(ObjectType $type): array
     {
         $reflection = Reflection::class($type->className());
         $methods = $reflection->getMethods();
@@ -134,7 +133,7 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
     /**
      * @param ReflectionClass<object> $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 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
     /**
      * @return array<string, Type>
      */
-    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 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
         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 85c6677..ea74709 100644
--- a/src/Library/Container.php
+++ b/src/Library/Container.php
@@ -33,11 +33,11 @@ use CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder;
 use CuyZ\Valinor\Mapper\Tree\Builder\InterfaceNodeBuilder;
 use CuyZ\Valinor\Mapper\Tree\Builder\IterableNodeBuilder;
 use CuyZ\Valinor\Mapper\Tree\Builder\ListNodeBuilder;
-use CuyZ\Valinor\Mapper\Tree\Builder\NativeClassNodeBuilder;
+use CuyZ\Valinor\Mapper\Tree\Builder\ObjectNodeBuilder;
 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\Normalizer;
 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 @@ final class Container
                     ShapedArrayType::class => new ShapedArrayNodeBuilder($settings->allowSuperfluousKeys),
                     ScalarType::class => new ScalarNodeBuilder($settings->enableFlexibleCasting),
                     NullType::class => new NullNodeBuilder(),
-                    ClassType::class => new NativeClassNodeBuilder(
+                    ObjectType::class => new ObjectNodeBuilder(
                         $this->get(ClassDefinitionRepository::class),
                         $this->get(ObjectBuilderFactory::class),
-                        $this->get(ObjectNodeBuilder::class),
+                        $this->get(FilteredObjectNodeBuilder::class),
                         $settings->enableFlexibleCasting,
                     ),
                 ]);
@@ -126,7 +126,11 @@ final class Container
                     $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 @@ final class Container
                 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 4c6bed8..22346f3 100644
--- a/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php
+++ b/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php
@@ -131,7 +131,7 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
         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 a3a54fe..fa5613a 100644
--- a/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php
+++ b/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php
@@ -12,7 +12,7 @@ use CuyZ\Valinor\Mapper\Object\DateTimeFormatConstructor;
 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 @@ final class DateTimeObjectBuilderFactory implements ObjectBuilderFactory
         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 7f76a43..cc17713 100644
--- a/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php
+++ b/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php
@@ -11,7 +11,7 @@ use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
 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 @@ final class DateTimeZoneObjectBuilderFactory implements ObjectBuilderFactory
         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 281f3e7..db1cc19 100644
--- a/src/Mapper/Object/FunctionObjectBuilder.php
+++ b/src/Mapper/Object/FunctionObjectBuilder.php
@@ -7,7 +7,7 @@ namespace CuyZ\Valinor\Mapper\Object;
 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/FilteredObjectNodeBuilder.php b/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php
new file mode 100644
index 0000000..712de13
--- /dev/null
+++ b/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Mapper\Tree\Builder;
+
+use CuyZ\Valinor\Mapper\Object\ArgumentsValues;
+use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
+use CuyZ\Valinor\Mapper\Tree\Exception\UnexpectedArrayKeysForClass;
+use CuyZ\Valinor\Mapper\Tree\Shell;
+
+use function count;
+
+/** @internal */
+final class FilteredObjectNodeBuilder
+{
+    public function __construct(private bool $allowSuperfluousKeys) {}
+
+    public function build(ObjectBuilder $builder, Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
+    {
+        $arguments = ArgumentsValues::forClass($builder->describeArguments(), $shell->value(), $this->allowSuperfluousKeys);
+
+        $children = $this->children($shell, $arguments, $rootBuilder);
+
+        $object = $this->buildObject($builder, $children);
+
+        $node = $arguments->hadSingleArgument()
+            ? TreeNode::flattenedBranch($shell, $object, $children[0])
+            : TreeNode::branch($shell, $object, $children);
+
+        if (! $this->allowSuperfluousKeys && count($arguments->superfluousKeys()) > 0) {
+            $node = $node->withMessage(new UnexpectedArrayKeysForClass($arguments));
+        }
+
+        return $node;
+    }
+
+    /**
+     * @return array<TreeNode>
+     */
+    private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array
+    {
+        $children = [];
+
+        foreach ($arguments as $argument) {
+            $name = $argument->name();
+            $type = $argument->type();
+            $attributes = $argument->attributes();
+
+            $child = $shell->child($name, $type, $attributes);
+
+            if ($arguments->hasValue($name)) {
+                $child = $child->withValue($arguments->getValue($name));
+            }
+
+            $children[] = $rootBuilder->build($child);
+        }
+
+        return $children;
+    }
+
+    /**
+     * @param TreeNode[] $children
+     */
+    private function buildObject(ObjectBuilder $builder, array $children): ?object
+    {
+        $arguments = [];
+
+        foreach ($children as $child) {
+            if (! $child->isValid()) {
+                return null;
+            }
+
+            $arguments[$child->name()] = $child->value();
+        }
+
+        return $builder->build($arguments);
+    }
+}
diff --git a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
index 36afb2a..b96b663 100644
--- a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 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\Factory\ObjectBuilderFactory;
 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 @@ final class InterfaceNodeBuilder implements NodeBuilder
         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 @@ final class InterfaceNodeBuilder implements NodeBuilder
             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 @@ final class InterfaceNodeBuilder implements NodeBuilder
 
         $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
deleted file mode 100644
index f6b2d76..0000000
--- a/src/Mapper/Tree/Builder/NativeClassNodeBuilder.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Mapper\Tree\Builder;
-
-use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
-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 function assert;
-
-/** @internal */
-final class NativeClassNodeBuilder implements NodeBuilder
-{
-    public function __construct(
-        private ClassDefinitionRepository $classDefinitionRepository,
-        private ObjectBuilderFactory $objectBuilderFactory,
-        private ObjectNodeBuilder $objectNodeBuilder,
-        private bool $enableFlexibleCasting,
-    ) {}
-
-    public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
-    {
-        $type = $shell->type();
-
-        // @infection-ignore-all
-        assert($type instanceof ClassType);
-
-        if ($this->enableFlexibleCasting && $shell->value() === null) {
-            $shell = $shell->withValue([]);
-        }
-
-        $class = $this->classDefinitionRepository->for($type);
-        $objectBuilder = FilteredObjectBuilder::from($shell->value(), ...$this->objectBuilderFactory->for($class));
-
-        return $this->objectNodeBuilder->build($objectBuilder, $shell, $rootBuilder);
-    }
-}
diff --git a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
index d060de9..e67b613 100644
--- a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
@@ -4,76 +4,39 @@ declare(strict_types=1);
 
 namespace CuyZ\Valinor\Mapper\Tree\Builder;
 
-use CuyZ\Valinor\Mapper\Object\ArgumentsValues;
-use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
-use CuyZ\Valinor\Mapper\Tree\Exception\UnexpectedArrayKeysForClass;
+use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
+use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
+use CuyZ\Valinor\Mapper\Object\FilteredObjectBuilder;
 use CuyZ\Valinor\Mapper\Tree\Shell;
 
-use function count;
+use CuyZ\Valinor\Type\ObjectType;
+
+use function assert;
 
 /** @internal */
-final class ObjectNodeBuilder
+final class ObjectNodeBuilder implements NodeBuilder
 {
-    public function __construct(private bool $allowSuperfluousKeys) {}
-
-    public function build(ObjectBuilder $builder, Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
+    public function __construct(
+        private ClassDefinitionRepository $classDefinitionRepository,
+        private ObjectBuilderFactory $objectBuilderFactory,
+        private FilteredObjectNodeBuilder $filteredObjectNodeBuilder,
+        private bool $enableFlexibleCasting,
+    ) {}
+
+    public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
     {
-        $arguments = ArgumentsValues::forClass($builder->describeArguments(), $shell->value(), $this->allowSuperfluousKeys);
-
-        $children = $this->children($shell, $arguments, $rootBuilder);
+        $type = $shell->type();
 
-        $object = $this->buildObject($builder, $children);
+        // @infection-ignore-all
+        assert($type instanceof ObjectType);
 
-        $node = $arguments->hadSingleArgument()
-            ? TreeNode::flattenedBranch($shell, $object, $children[0])
-            : TreeNode::branch($shell, $object, $children);
-
-        if (! $this->allowSuperfluousKeys && count($arguments->superfluousKeys()) > 0) {
-            $node = $node->withMessage(new UnexpectedArrayKeysForClass($arguments));
+        if ($this->enableFlexibleCasting && $shell->value() === null) {
+            $shell = $shell->withValue([]);
         }
 
-        return $node;
-    }
-
-    /**
-     * @return array<TreeNode>
-     */
-    private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array
-    {
-        $children = [];
-
-        foreach ($arguments as $argument) {
-            $name = $argument->name();
-            $type = $argument->type();
-            $attributes = $argument->attributes();
-
-            $child = $shell->child($name, $type, $attributes);
-
-            if ($arguments->hasValue($name)) {
-                $child = $child->withValue($arguments->getValue($name));
-            }
-
-            $children[] = $rootBuilder->build($child);
-        }
-
-        return $children;
-    }
-
-    /**
-     * @param TreeNode[] $children
-     */
-    private function buildObject(ObjectBuilder $builder, array $children): ?object
-    {
-        $arguments = [];
-
-        foreach ($children as $child) {
-            if (! $child->isValid()) {
-                return null;
-            }
-
-            $arguments[$child->name()] = $child->value();
-        }
+        $class = $this->classDefinitionRepository->for($type);
+        $objectBuilder = FilteredObjectBuilder::from($shell->value(), ...$this->objectBuilderFactory->for($class));
 
-        return $builder->build($arguments);
+        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 0000000..77cb985
--- /dev/null
+++ b/src/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Mapper\Tree\Exception;
+
+use LogicException;
+
+/** @internal */
+final class InterfaceHasBothConstructorAndInfer extends LogicException
+{
+    /**
+     * @param interface-string $name
+     */
+    public function __construct(string $name)
+    {
+        parent::__construct(
+            "Interface `$name` is configured with at least one constructor but also has an infer configuration. Only one method can be used.",
+            1711915749,
+        );
+    }
+}
diff --git a/src/Type/Types/InterfaceType.php b/src/Type/Types/InterfaceType.php
index 6c5c4ff..b94cfdd 100644
--- a/src/Type/Types/InterfaceType.php
+++ b/src/Type/Types/InterfaceType.php
@@ -51,7 +51,7 @@ final class InterfaceType implements ObjectType, GenericType
             return false;
         }
 
-        return is_a($other->className(), $this->interfaceName, true);
+        return is_a($this->interfaceName, $other->className(), true);
     }
 
     public function traverse(): array


Test patch:
diff --git a/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php b/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php
index 015f5dc..727e45f 100644
--- a/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php
+++ b/tests/Fake/Definition/Repository/FakeClassDefinitionRepository.php
@@ -7,11 +7,11 @@ namespace CuyZ\Valinor\Tests\Fake\Definition\Repository;
 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 893c502..6d1b1b1 100644
--- a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php
+++ b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php
@@ -12,6 +12,7 @@ use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorClassTypeParameter;
 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 @@ final class ConstructorRegistrationMappingTest extends IntegrationTestCase
             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 @@ final class SomeClassProvidingStaticClosure
         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 597fd44..9f2837d 100644
--- a/tests/Unit/Type/Types/InterfaceTypeTest.php
+++ b/tests/Unit/Type/Types/InterfaceTypeTest.php
@@ -66,8 +66,8 @@ final class InterfaceTypeTest extends TestCase
 
     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 @@ final class InterfaceTypeTest extends TestCase
     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 @@ final class InterfaceTypeTest extends TestCase
         self::assertFalse((new InterfaceType(DateTime::class))->matches($intersectionType));
     }
 }
+
+interface SomeParentInterface {}
+
+interface SomeOtherParentInterface {}
+
+interface SomeChildInterface extends SomeParentInterface, SomeOtherParentInterface {}