From b2e810e3ce997181b7cfcc96d2bccb9c56a5bdd8 Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Wed, 29 Dec 2021 00:09:34 +0100 Subject: [PATCH] feat!: allow mapping to any type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the method `TreeMapper::map` would allow mapping only to an object. It is now possible to map to any type handled by the library. It is for instance possible to map to an array of objects: ```php $objects = (new \CuyZ\Valinor\MapperBuilder())->mapper()->map( 'array<' . SomeClass::class . '>', [/* … */] ); ``` For simple use-cases, an array shape can be used: ```php $array = (new \CuyZ\Valinor\MapperBuilder())->mapper()->map( 'array{foo: string, bar: int}', [/* … */] ); echo strtolower($array['foo']); echo $array['bar'] * 2; ``` This new feature changes the possible behaviour of the mapper, meaning static analysis tools need help to understand the types correctly. An extension for PHPStan and a plugin for Psalm are now provided and can be included in a project to automatically increase the type coverage. --- .php-cs-fixer.dist.php | 3 +- README.md | 125 +++++++++++++++++- composer.json | 3 +- phpstan.neon.dist | 8 +- .../Extension/TreeMapperPHPStanExtension.php | 62 +++++++++ .../Psr/SimpleCache/CacheInterface.stub | 0 qa/PHPStan/valinor-phpstan-configuration.php | 14 ++ qa/Psalm/Plugin/TreeMapperPsalmPlugin.php | 68 ++++++++++ src/Mapper/Exception/InvalidMappingType.php | 19 --- src/Mapper/MappingError.php | 2 +- src/Mapper/TreeMapper.php | 6 +- src/Mapper/TreeMapperContainer.php | 10 +- tests/Fake/Mapper/FakeTreeMapper.php | 31 ----- .../ArrayValuesMappingTest.php | 2 +- .../{Type => Object}/DateTimeMappingTest.php | 2 +- .../EnumValuesMappingTest.php | 2 +- .../GenericValuesMappingTest.php | 2 +- .../IterableValuesMappingTest.php | 2 +- .../ListValuesMappingTest.php | 3 +- .../LocalTypeAliasMappingTest.php | 2 +- .../ObjectValuesMappingTest.php | 2 +- .../ScalarValuesMappingTest.php | 2 +- .../ShapedArrayValuesMappingTest.php | 2 +- .../UnionValuesMappingTest.php | 2 +- .../Other/ArrayOfScalarMappingTest.php | 28 ++++ .../Mapping/Other/ShapedArrayMappingTest.php | 49 +++++++ tests/Unit/Mapper/TreeMapperContainerTest.php | 12 +- 27 files changed, 374 insertions(+), 89 deletions(-) create mode 100644 qa/PHPStan/Extension/TreeMapperPHPStanExtension.php rename {stubs => qa/PHPStan/Stubs}/Psr/SimpleCache/CacheInterface.stub (100%) create mode 100644 qa/PHPStan/valinor-phpstan-configuration.php create mode 100644 qa/Psalm/Plugin/TreeMapperPsalmPlugin.php delete mode 100644 src/Mapper/Exception/InvalidMappingType.php delete mode 100644 tests/Fake/Mapper/FakeTreeMapper.php rename tests/Integration/Mapping/{Type => Object}/ArrayValuesMappingTest.php (99%) rename tests/Integration/Mapping/{Type => Object}/DateTimeMappingTest.php (98%) rename tests/Integration/Mapping/{Type => Object}/EnumValuesMappingTest.php (98%) rename tests/Integration/Mapping/{Type => Object}/GenericValuesMappingTest.php (99%) rename tests/Integration/Mapping/{Type => Object}/IterableValuesMappingTest.php (98%) rename tests/Integration/Mapping/{Type => Object}/ListValuesMappingTest.php (99%) rename tests/Integration/Mapping/{Type => Object}/LocalTypeAliasMappingTest.php (98%) rename tests/Integration/Mapping/{Type => Object}/ObjectValuesMappingTest.php (94%) rename tests/Integration/Mapping/{Type => Object}/ScalarValuesMappingTest.php (99%) rename tests/Integration/Mapping/{Type => Object}/ShapedArrayValuesMappingTest.php (98%) rename tests/Integration/Mapping/{Type => Object}/UnionValuesMappingTest.php (98%) create mode 100644 tests/Integration/Mapping/Other/ArrayOfScalarMappingTest.php create mode 100644 tests/Integration/Mapping/Other/ShapedArrayMappingTest.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 8ddad24..3e284d8 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,7 +2,8 @@ $finder = PhpCsFixer\Finder::create()->in([ './src', - './tests' + './tests', + './qa', ]); if (PHP_VERSION_ID < 8_00_00) { diff --git a/README.md b/README.md index 5751e76..7e58ce3 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,44 @@ public function getThread(int $id): Thread } ``` +### Mapping advanced types + +Although it is recommended to map an input to a value object, in some cases +mapping to another type can be easier/more flexible. + +It is for instance possible to map to an array of objects: + +```php +try { + $objects = (new \CuyZ\Valinor\MapperBuilder()) + ->mapper() + ->map( + 'array<' . SomeClass::class . '>', + [/* … */] + ); +} catch (\CuyZ\Valinor\Mapper\MappingError $error) { + // Do something… +} +``` + +For simple use-cases, an array shape can be used: + +```php +try { + $array = (new \CuyZ\Valinor\MapperBuilder()) + ->mapper() + ->map( + 'array{foo: string, bar: int}', + [/* … */] + ); + + echo $array['foo']; + echo $array['bar'] * 2; +} catch (\CuyZ\Valinor\Mapper\MappingError $error) { + // Do something… +} +``` + ### Validation The source given to a mapper can never be trusted, this is actually the very @@ -225,7 +263,7 @@ map(new \CuyZ\Valinor\Mapper\Source\FileSource( ### Construction strategy During the mapping, instances of the objects are created and hydrated with the -correct values. construction strategies will determine what values are needed +correct values. Construction strategies will determine what values are needed and how an object is built. An object can provide either… @@ -370,6 +408,91 @@ final class SomeClass } ``` +## Static analysis + +To help static analysis of a codebase using this library, an extension for +[PHPStan] and a plugin for [Psalm] are provided. They enable these tools to +better understand the behaviour of the mapper. + +Considering at least one of those tools are installed on a project, below are +examples of the kind of errors that would be reported. + +**Mapping to an array of classes** + +```php +final class SomeClass +{ + public function __construct( + public readonly string $foo, + public readonly int $bar, + ) {} +} + +$objects = (new \CuyZ\Valinor\MapperBuilder()) + ->mapper() + ->map( + 'array<' . SomeClass::class . '>', + [/* … */] + ); + +foreach ($objects as $object) { + // ✅ + echo $object->foo; + + // ✅ + echo $object->bar * 2; + + // ❌ Cannot perform operation between `string` and `int` + echo $object->foo * $object->bar; + + // ❌ Property `SomeClass::$fiz` is not defined + echo $object->fiz; +} +``` + +**Mapping to a shaped array** + +```php +$array = (new \CuyZ\Valinor\MapperBuilder()) + ->mapper() + ->map( + 'array{foo: string, bar: int}', + [/* … */] + ); + +// ✅ +echo $array['foo']; + +// ❌ Expected `string` but got `int` +echo strtolower($array['bar']); + +// ❌ Cannot perform operation between `string` and `int` +echo $array['foo'] * $array['bar']; + +// ❌ Offset `fiz` does not exist on array +echo $array['fiz']; +``` + +--- + +To activate this feature, the configuration must be updated for the installed +tool(s): + +**PHPStan** + +```yaml +includes: + - vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php +``` + +**Psalm** + +```xml + + + +``` + [PHPStan]: https://phpstan.org/ [Psalm]: https://psalm.dev/ diff --git a/composer.json b/composer.json index fb64645..b8add7e 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ }, "autoload-dev": { "psr-4": { - "CuyZ\\Valinor\\Tests\\": "tests" + "CuyZ\\Valinor\\Tests\\": "tests", + "CuyZ\\Valinor\\QA\\": "qa" } }, "scripts": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4da6dab..d8b98d2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,5 @@ includes: + - qa/PHPStan/valinor-phpstan-configuration.php - vendor/phpstan/phpstan-strict-rules/rules.neon parameters: @@ -6,6 +7,7 @@ parameters: paths: - src - tests + - qa/PHPStan ignoreErrors: # \PHPStan\Rules\BooleansInConditions - '#Only booleans are allowed in .* given#' @@ -15,6 +17,10 @@ parameters: - '#Construct empty\(\) is not allowed\. Use more strict comparison\.#' - '#Method [\w\\:]+_data_provider\(\) return type has no value type specified in iterable type#' + + - message: '#Template type T of method CuyZ\\Valinor\\Mapper\\TreeMapper::map\(\) is not referenced in a parameter#' + path: src/Mapper/TreeMapper.php + stubFiles: - - stubs/Psr/SimpleCache/CacheInterface.stub + - qa/PHPStan/Stubs/Psr/SimpleCache/CacheInterface.stub tmpDir: var/cache/phpstan diff --git a/qa/PHPStan/Extension/TreeMapperPHPStanExtension.php b/qa/PHPStan/Extension/TreeMapperPHPStanExtension.php new file mode 100644 index 0000000..67ddbd2 --- /dev/null +++ b/qa/PHPStan/Extension/TreeMapperPHPStanExtension.php @@ -0,0 +1,62 @@ +resolver = $resolver; + } + + public function getClass(): string + { + return TreeMapper::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'map'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $argument = $methodCall->getArgs()[0]->value; + $type = $scope->getType($argument); + + if ($type instanceof UnionType) { + return $type->traverse(fn (Type $type) => $this->type($type)); + } + + return $this->type($type); + } + + private function type(Type $type): Type + { + if ($type instanceof GenericClassStringType) { + return $type->getGenericType(); + } + + if ($type instanceof ConstantStringType) { + return $this->resolver->resolve($type->getValue()); + } + + return new MixedType(); + } +} diff --git a/stubs/Psr/SimpleCache/CacheInterface.stub b/qa/PHPStan/Stubs/Psr/SimpleCache/CacheInterface.stub similarity index 100% rename from stubs/Psr/SimpleCache/CacheInterface.stub rename to qa/PHPStan/Stubs/Psr/SimpleCache/CacheInterface.stub diff --git a/qa/PHPStan/valinor-phpstan-configuration.php b/qa/PHPStan/valinor-phpstan-configuration.php new file mode 100644 index 0000000..052f9f6 --- /dev/null +++ b/qa/PHPStan/valinor-phpstan-configuration.php @@ -0,0 +1,14 @@ + [ + [ + 'class' => TreeMapperPHPStanExtension::class, + 'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension'] + ] + ], +]; diff --git a/qa/Psalm/Plugin/TreeMapperPsalmPlugin.php b/qa/Psalm/Plugin/TreeMapperPsalmPlugin.php new file mode 100644 index 0000000..8d26850 --- /dev/null +++ b/qa/Psalm/Plugin/TreeMapperPsalmPlugin.php @@ -0,0 +1,68 @@ +getMethodNameLowercase() !== 'map') { + return null; + } + + $type = $event->getSource()->getNodeTypeProvider()->getType($event->getCallArgs()[0]->value); + + if (! $type) { + return null; + } + + $types = []; + + foreach ($type->getChildNodes() as $node) { + $inferred = self::type($node); + + if ($inferred === null) { + return null; + } + + $types[] = $inferred; + } + + if (count($types) === 0) { + return null; + } + + return Type::combineUnionTypeArray($types, $event->getSource()->getCodebase()); + } + + private static function type(Atomic $node): ?Union + { + switch (true) { + case $node instanceof TLiteralString: + return Type::parseString($node->value); + case $node instanceof TDependentGetClass: + return $node->as_type; + case $node instanceof TClassString && $node->as_type: + return new Union([$node->as_type]); + default: + return null; + } + } +} diff --git a/src/Mapper/Exception/InvalidMappingType.php b/src/Mapper/Exception/InvalidMappingType.php deleted file mode 100644 index f25302d..0000000 --- a/src/Mapper/Exception/InvalidMappingType.php +++ /dev/null @@ -1,19 +0,0 @@ -node = $node; parent::__construct( - "Could not map an object of type `{$node->type()}` with the given source.", + "Could not map type `{$node->type()}` with the given source.", 1617193185 ); } diff --git a/src/Mapper/TreeMapper.php b/src/Mapper/TreeMapper.php index 2df7f51..737857d 100644 --- a/src/Mapper/TreeMapper.php +++ b/src/Mapper/TreeMapper.php @@ -9,11 +9,11 @@ interface TreeMapper /** * @template T of object * - * @param class-string $signature + * @param string|class-string $signature * @param mixed $source - * @return T + * @return T|mixed * * @throws MappingError */ - public function map(string $signature, $source): object; + public function map(string $signature, $source); } diff --git a/src/Mapper/TreeMapperContainer.php b/src/Mapper/TreeMapperContainer.php index d5af17d..8bd7b56 100644 --- a/src/Mapper/TreeMapperContainer.php +++ b/src/Mapper/TreeMapperContainer.php @@ -4,12 +4,10 @@ declare(strict_types=1); namespace CuyZ\Valinor\Mapper; -use CuyZ\Valinor\Mapper\Exception\InvalidMappingType; use CuyZ\Valinor\Mapper\Exception\InvalidMappingTypeSignature; use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Node; use CuyZ\Valinor\Mapper\Tree\Shell; -use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use CuyZ\Valinor\Type\Parser\TypeParser; @@ -25,7 +23,7 @@ final class TreeMapperContainer implements TreeMapper $this->nodeBuilder = $nodeBuilder; } - public function map(string $signature, $source): object + public function map(string $signature, $source) { $node = $this->node($signature, $source); @@ -33,7 +31,7 @@ final class TreeMapperContainer implements TreeMapper throw new MappingError($node); } - return $node->value(); // @phpstan-ignore-line + return $node->value(); } /** @@ -47,10 +45,6 @@ final class TreeMapperContainer implements TreeMapper throw new InvalidMappingTypeSignature($signature, $exception); } - if (! $type instanceof ObjectType) { - throw new InvalidMappingType($type); - } - $shell = Shell::root($type, $source); return $this->nodeBuilder->build($shell); diff --git a/tests/Fake/Mapper/FakeTreeMapper.php b/tests/Fake/Mapper/FakeTreeMapper.php deleted file mode 100644 index bf04c00..0000000 --- a/tests/Fake/Mapper/FakeTreeMapper.php +++ /dev/null @@ -1,31 +0,0 @@ - */ - private array $objects = []; - - /** - * @param mixed $source - * @phpstan-return object - */ - public function map(string $signature, $source): object - { - return $this->objects[$signature] ?? new stdClass(); - } - - /** - * @param class-string $signature - */ - public function willReturn(string $signature, object $object): void - { - $this->objects[$signature] = $object; - } -} diff --git a/tests/Integration/Mapping/Type/ArrayValuesMappingTest.php b/tests/Integration/Mapping/Object/ArrayValuesMappingTest.php similarity index 99% rename from tests/Integration/Mapping/Type/ArrayValuesMappingTest.php rename to tests/Integration/Mapping/Object/ArrayValuesMappingTest.php index 13af61d..26f834f 100644 --- a/tests/Integration/Mapping/Type/ArrayValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ArrayValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Mapper\Tree\Exception\CannotCastToScalarValue; diff --git a/tests/Integration/Mapping/Type/DateTimeMappingTest.php b/tests/Integration/Mapping/Object/DateTimeMappingTest.php similarity index 98% rename from tests/Integration/Mapping/Type/DateTimeMappingTest.php rename to tests/Integration/Mapping/Object/DateTimeMappingTest.php index e7ab185..bcee1b2 100644 --- a/tests/Integration/Mapping/Type/DateTimeMappingTest.php +++ b/tests/Integration/Mapping/Object/DateTimeMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Mapper\Object\DateTimeObjectBuilder; diff --git a/tests/Integration/Mapping/Type/EnumValuesMappingTest.php b/tests/Integration/Mapping/Object/EnumValuesMappingTest.php similarity index 98% rename from tests/Integration/Mapping/Type/EnumValuesMappingTest.php rename to tests/Integration/Mapping/Object/EnumValuesMappingTest.php index 2c85e33..95852e6 100644 --- a/tests/Integration/Mapping/Type/EnumValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/EnumValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum; diff --git a/tests/Integration/Mapping/Type/GenericValuesMappingTest.php b/tests/Integration/Mapping/Object/GenericValuesMappingTest.php similarity index 99% rename from tests/Integration/Mapping/Type/GenericValuesMappingTest.php rename to tests/Integration/Mapping/Object/GenericValuesMappingTest.php index 7494030..ba1ab14 100644 --- a/tests/Integration/Mapping/Type/GenericValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/GenericValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Tests\Integration\IntegrationTest; diff --git a/tests/Integration/Mapping/Type/IterableValuesMappingTest.php b/tests/Integration/Mapping/Object/IterableValuesMappingTest.php similarity index 98% rename from tests/Integration/Mapping/Type/IterableValuesMappingTest.php rename to tests/Integration/Mapping/Object/IterableValuesMappingTest.php index d297e00..ec52e84 100644 --- a/tests/Integration/Mapping/Type/IterableValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/IterableValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Tests\Integration\IntegrationTest; diff --git a/tests/Integration/Mapping/Type/ListValuesMappingTest.php b/tests/Integration/Mapping/Object/ListValuesMappingTest.php similarity index 99% rename from tests/Integration/Mapping/Type/ListValuesMappingTest.php rename to tests/Integration/Mapping/Object/ListValuesMappingTest.php index 23b2cd6..9731d54 100644 --- a/tests/Integration/Mapping/Type/ListValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ListValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Mapper\Tree\Exception\CannotCastToScalarValue; @@ -10,7 +10,6 @@ use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeValue; use CuyZ\Valinor\Tests\Integration\IntegrationTest; use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\SimpleObject; use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\SimpleObject as SimpleObjectAlias; - use Throwable; use function array_values; diff --git a/tests/Integration/Mapping/Type/LocalTypeAliasMappingTest.php b/tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php similarity index 98% rename from tests/Integration/Mapping/Type/LocalTypeAliasMappingTest.php rename to tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php index 740fdb7..80a1175 100644 --- a/tests/Integration/Mapping/Type/LocalTypeAliasMappingTest.php +++ b/tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Tests\Integration\IntegrationTest; diff --git a/tests/Integration/Mapping/Type/ObjectValuesMappingTest.php b/tests/Integration/Mapping/Object/ObjectValuesMappingTest.php similarity index 94% rename from tests/Integration/Mapping/Type/ObjectValuesMappingTest.php rename to tests/Integration/Mapping/Object/ObjectValuesMappingTest.php index 0cfb07b..89a5ca4 100644 --- a/tests/Integration/Mapping/Type/ObjectValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ObjectValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Tests\Integration\IntegrationTest; diff --git a/tests/Integration/Mapping/Type/ScalarValuesMappingTest.php b/tests/Integration/Mapping/Object/ScalarValuesMappingTest.php similarity index 99% rename from tests/Integration/Mapping/Type/ScalarValuesMappingTest.php rename to tests/Integration/Mapping/Object/ScalarValuesMappingTest.php index bf39947..6174e38 100644 --- a/tests/Integration/Mapping/Type/ScalarValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ScalarValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Mapper\Tree\Exception\CannotCastToScalarValue; diff --git a/tests/Integration/Mapping/Type/ShapedArrayValuesMappingTest.php b/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php similarity index 98% rename from tests/Integration/Mapping/Type/ShapedArrayValuesMappingTest.php rename to tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php index 571338a..3603a08 100644 --- a/tests/Integration/Mapping/Type/ShapedArrayValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Mapper\Tree\Exception\CannotCastToScalarValue; diff --git a/tests/Integration/Mapping/Type/UnionValuesMappingTest.php b/tests/Integration/Mapping/Object/UnionValuesMappingTest.php similarity index 98% rename from tests/Integration/Mapping/Type/UnionValuesMappingTest.php rename to tests/Integration/Mapping/Object/UnionValuesMappingTest.php index ea21ec0..cd64ada 100644 --- a/tests/Integration/Mapping/Type/UnionValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/UnionValuesMappingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Integration\Mapping\Type; +namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Tests\Integration\IntegrationTest; diff --git a/tests/Integration/Mapping/Other/ArrayOfScalarMappingTest.php b/tests/Integration/Mapping/Other/ArrayOfScalarMappingTest.php new file mode 100644 index 0000000..a78701f --- /dev/null +++ b/tests/Integration/Mapping/Other/ArrayOfScalarMappingTest.php @@ -0,0 +1,28 @@ +mapperBuilder->mapper()->map('string[]', $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(['foo', '42', '1337.404'], $result); + } +} diff --git a/tests/Integration/Mapping/Other/ShapedArrayMappingTest.php b/tests/Integration/Mapping/Other/ShapedArrayMappingTest.php new file mode 100644 index 0000000..2f39d14 --- /dev/null +++ b/tests/Integration/Mapping/Other/ShapedArrayMappingTest.php @@ -0,0 +1,49 @@ + 'foo', + 'bar' => '42', + 'fiz' => '1337.404', + ]; + + try { + $result = $this->mapperBuilder->mapper()->map('array{foo: string, bar: int, fiz: float}', $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result['foo']); + self::assertSame(42, $result['bar']); + self::assertSame(1337.404, $result['fiz']); + } + + public function test_shared_values_are_mapped_properly(): void + { + $source = [ + 'foo' => 'foo', + 'bar' => '42', + 'fiz' => '1337.404', + ]; + + foreach (['array{foo: string, bar: int}', 'array{bar: int, fiz:float}'] as $signature) { + try { + $result = $this->mapperBuilder->mapper()->map($signature, $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(42, $result['bar']); + } + } +} diff --git a/tests/Unit/Mapper/TreeMapperContainerTest.php b/tests/Unit/Mapper/TreeMapperContainerTest.php index 16c8ac0..9ac3860 100644 --- a/tests/Unit/Mapper/TreeMapperContainerTest.php +++ b/tests/Unit/Mapper/TreeMapperContainerTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace CuyZ\Valinor\Tests\Unit\Mapper; -use CuyZ\Valinor\Mapper\Exception\InvalidMappingType; use CuyZ\Valinor\Mapper\Exception\InvalidMappingTypeSignature; use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder; use CuyZ\Valinor\Mapper\TreeMapperContainer; @@ -32,15 +31,6 @@ final class TreeMapperContainerTest extends TestCase $this->expectExceptionCode(1630959692); $this->expectExceptionMessageMatches('/^Could not parse the type `foo` that should be mapped: .*/'); - $this->mapper->map('foo', []); // @phpstan-ignore-line - } - - public function test_invalid_mapping_type_throws_exception(): void - { - $this->expectException(InvalidMappingType::class); - $this->expectExceptionCode(1630959731); - $this->expectExceptionMessage("Can not map type `string`, it must be an object type."); - - $this->mapper->map('string', []); // @phpstan-ignore-line + $this->mapper->map('foo', []); } }