Repository: CuyZ/Valinor (php)

Issue:
Multidimensional arrays don't work
```
final class SomeClass
{
    // If I don't comment then it works fine
    //public readonly string $description;

    /** @var Identifier[] */
    public array $identifier;

}

final class Identifier
{
    public string $value;
}

$mapper = (new \CuyZ\Valinor\MapperBuilder())->allowSuperfluousKeys()->mapper();
$result = $mapper->map(SomeClass::class, [
    'identifier' => [
        ['value' => 'some-identifier'],
        ['value' => 'some-identifier2']
    ],
    'description' => 'Lorem ipsum…',
]);
```

```
Fatal error: Uncaught CuyZ\Valinor\Mapper\TypeTreeMapperError: Could not map type `SomeClass`. An error occurred at path identifier.identifier: Value array{0: array{…}, 1: array{…}} is not a valid string
```

Pull request:
fix: handle single array mapping when a superfluous value is present
Fixes #453

Patch:
diff --git a/src/Library/Container.php b/src/Library/Container.php
index c87823e..8ba5601 100644
--- a/src/Library/Container.php
+++ b/src/Library/Container.php
@@ -130,7 +130,8 @@ final class Container
                     $this->get(ClassDefinitionRepository::class),
                     $this->get(ObjectBuilderFactory::class),
                     $this->get(ObjectNodeBuilder::class),
-                    $settings->enableFlexibleCasting
+                    $settings->enableFlexibleCasting,
+                    $settings->allowSuperfluousKeys,
                 );
 
                 $builder = new CasterProxyNodeBuilder($builder);
diff --git a/src/Mapper/Object/ArgumentsValues.php b/src/Mapper/Object/ArgumentsValues.php
index 6a47e0e..f2d7aee 100644
--- a/src/Mapper/Object/ArgumentsValues.php
+++ b/src/Mapper/Object/ArgumentsValues.php
@@ -36,22 +36,22 @@ final class ArgumentsValues implements IteratorAggregate
         $this->arguments = $arguments;
     }
 
-    public static function forInterface(Arguments $arguments, mixed $value): self
+    public static function forInterface(Arguments $arguments, mixed $value, bool $allowSuperfluousKeys): self
     {
         $self = new self($arguments);
         $self->forInterface = true;
 
         if (count($arguments) > 0) {
-            $self = $self->transform($value);
+            $self = $self->transform($value, $allowSuperfluousKeys);
         }
 
         return $self;
     }
 
-    public static function forClass(Arguments $arguments, mixed $value): self
+    public static function forClass(Arguments $arguments, mixed $value, bool $allowSuperfluousKeys): self
     {
         $self = new self($arguments);
-        $self = $self->transform($value);
+        $self = $self->transform($value, $allowSuperfluousKeys);
 
         return $self;
     }
@@ -82,11 +82,11 @@ final class ArgumentsValues implements IteratorAggregate
         return $this->hadSingleArgument;
     }
 
-    private function transform(mixed $value): self
+    private function transform(mixed $value, bool $allowSuperfluousKeys): self
     {
         $clone = clone $this;
 
-        $transformedValue = $this->transformValueForSingleArgument($value);
+        $transformedValue = $this->transformValueForSingleArgument($value, $allowSuperfluousKeys);
 
         if (! is_array($transformedValue)) {
             throw new InvalidSource($transformedValue, $this->arguments);
@@ -109,7 +109,7 @@ final class ArgumentsValues implements IteratorAggregate
         return $clone;
     }
 
-    private function transformValueForSingleArgument(mixed $value): mixed
+    private function transformValueForSingleArgument(mixed $value, bool $allowSuperfluousKeys): mixed
     {
         if (count($this->arguments) !== 1) {
             return $value;
@@ -122,7 +122,7 @@ final class ArgumentsValues implements IteratorAggregate
             && $type->keyType() !== ArrayKeyType::integer();
 
         if (is_array($value) && array_key_exists($name, $value)) {
-            if ($this->forInterface || ! $isTraversableAndAllowsStringKeys || count($value) === 1) {
+            if ($this->forInterface || ! $isTraversableAndAllowsStringKeys || $allowSuperfluousKeys || count($value) === 1) {
                 return $value;
             }
         }
diff --git a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
index 93f3e89..c513883 100644
--- a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
@@ -26,7 +26,8 @@ final class InterfaceNodeBuilder implements NodeBuilder
         private ClassDefinitionRepository $classDefinitionRepository,
         private ObjectBuilderFactory $objectBuilderFactory,
         private ObjectNodeBuilder $objectNodeBuilder,
-        private bool $enableFlexibleCasting
+        private bool $enableFlexibleCasting,
+        private bool $allowSuperfluousKeys,
     ) {}
 
     public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
@@ -116,7 +117,7 @@ final class InterfaceNodeBuilder implements NodeBuilder
      */
     private function children(Shell $shell, Arguments $arguments, RootNodeBuilder $rootBuilder): array
     {
-        $arguments = ArgumentsValues::forInterface($arguments, $shell->value());
+        $arguments = ArgumentsValues::forInterface($arguments, $shell->value(), $this->allowSuperfluousKeys);
 
         $children = [];
 
diff --git a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
index e0965e5..d060de9 100644
--- a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
@@ -18,7 +18,7 @@ final class ObjectNodeBuilder
 
     public function build(ObjectBuilder $builder, Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
     {
-        $arguments = ArgumentsValues::forClass($builder->describeArguments(), $shell->value());
+        $arguments = ArgumentsValues::forClass($builder->describeArguments(), $shell->value(), $this->allowSuperfluousKeys);
 
         $children = $this->children($shell, $arguments, $rootBuilder);
 


Test patch:
diff --git a/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php b/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php
index 489360b..e02816f 100644
--- a/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php
+++ b/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php
@@ -105,6 +105,26 @@ final class SuperfluousKeysMappingTest extends IntegrationTestCase
 
         self::assertSame([7, 8, 9], $result->values);
     }
+
+    public function test_object_with_one_array_property_can_be_mapped_when_superfluous_key_is_present(): void
+    {
+        $class = new class () {
+            /** @var array<string> */
+            public array $values;
+        };
+
+        try {
+            $result = $this->mapper->map($class::class, [
+                'values' => ['foo', 'bar'],
+                'superfluous_key' => 'useless value',
+            ]);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+
+        self::assertSame('foo', $result->values[0]);
+        self::assertSame('bar', $result->values[1]);
+    }
 }
 
 final class UnionOfBarAndFizAndFoo