From 549e5fe183be2191501af8f98df00a84f97ee990 Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Tue, 8 Nov 2022 13:16:44 +0100 Subject: [PATCH] feat: split mapper flexible mode in three distinct modes The flexible mode has been deprecated in favor of three modes with distinct roles. 1. **The flexible casting** Changes the behaviours explained below: ```php $flexibleMapper = (new \CuyZ\Valinor\MapperBuilder()) ->enableFlexibleCasting() ->mapper(); // --- // Scalar types will accept non-strict values; for instance an // integer type will accept any valid numeric value like the // *string* "42". $flexibleMapper->map('int', '42'); // => 42 // --- // List type will accept non-incremental keys. $flexibleMapper->map('list', ['foo' => 42, 'bar' => 1337]); // => [0 => 42, 1 => 1338] // --- // If a value is missing in a source for a node that accepts `null`, // the node will be filled with `null`. $flexibleMapper->map( 'array{foo: string, bar: null|string}', ['foo' => 'foo'] // `bar` is missing ); // => ['foo' => 'foo', 'bar' => null] // --- // Array and list types will convert `null` or missing values to an // empty array. $flexibleMapper->map( 'array{foo: string, bar: array}', ['foo' => 'foo'] // `bar` is missing ); // => ['foo' => 'foo', 'bar' => []] ``` 2. **The superfluous keys** Superfluous keys in source arrays will be allowed, preventing errors when a value is not bound to any object property/parameter or shaped array element. ```php (new \CuyZ\Valinor\MapperBuilder()) ->allowSuperfluousKeys() ->mapper() ->map( 'array{foo: string, bar: int}', [ 'foo' => 'foo', 'bar' => 42, 'baz' => 1337.404, // `baz` will be ignored ] ); ``` 3. **The permissive types** Allows permissive types `mixed` and `object` to be used during mapping. ```php (new \CuyZ\Valinor\MapperBuilder()) ->allowPermissiveTypes() ->mapper() ->map( 'array{foo: string, bar: mixed}', [ 'foo' => 'foo', 'bar' => 42, // Could be any value ] ); ``` --- docs/pages/mapping/type-strictness.md | 98 +++++-- src/Library/Container.php | 19 +- src/Library/Settings.php | 6 +- src/Mapper/Object/FilledArguments.php | 17 +- src/Mapper/Tree/Builder/ArrayNodeBuilder.php | 8 +- src/Mapper/Tree/Builder/ClassNodeBuilder.php | 22 +- .../Tree/Builder/InterfaceNodeBuilder.php | 12 +- src/Mapper/Tree/Builder/ListNodeBuilder.php | 10 +- src/Mapper/Tree/Builder/ScalarNodeBuilder.php | 8 +- .../Tree/Builder/ShapedArrayNodeBuilder.php | 8 +- src/Mapper/Tree/Builder/StrictNodeBuilder.php | 13 +- src/Mapper/Tree/Builder/UnionNodeBuilder.php | 8 +- src/MapperBuilder.php | 119 +++++++- .../Other/FlexibleCastingMappingTest.php | 269 ++++++++++++++++++ .../Mapping/Other/FlexibleMappingTest.php | 200 ------------- .../Other/PermissiveTypesMappingTest.php | 51 ++++ .../Mapping/Other/StrictMappingTest.php | 183 ++++++++++++ .../Other/SuperfluousKeysMappingTest.php | 58 ++++ tests/Unit/MapperBuilderTest.php | 14 +- 19 files changed, 829 insertions(+), 294 deletions(-) create mode 100644 tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php create mode 100644 tests/Integration/Mapping/Other/PermissiveTypesMappingTest.php create mode 100644 tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php diff --git a/docs/pages/mapping/type-strictness.md b/docs/pages/mapping/type-strictness.md index 2fc92c86..0a6b3d94 100644 --- a/docs/pages/mapping/type-strictness.md +++ b/docs/pages/mapping/type-strictness.md @@ -9,34 +9,96 @@ Array keys that are not bound to any node are forbidden. Mapping an array `bar` will fail, because `baz` is superfluous. The same rule applies for shaped arrays. +When mapping to a list, the given array must have sequential integer keys +starting at 0; if any gap or invalid key is found it will fail, like for +instance trying to map `['foo' => 'foo', 'bar' => 'bar']` to `list`. + Types that are too permissive are not permitted — if the mapper encounters a type like `mixed`, `object` or `array` it will fail because those types are not precise enough. -## Flexible mode +--- + +If these limitations are too restrictive, the mapper can be made more flexible +to disable one or several rule(s) declared above. + +## Enabling flexible casting + +This setting changes the behaviours explained below: + +```php +$flexibleMapper = (new \CuyZ\Valinor\MapperBuilder()) + ->enableFlexibleCasting() + ->mapper(); + +// --- +// Scalar types will accept non-strict values; for instance an integer +// type will accept any valid numeric value like the *string* "42". + +$flexibleMapper->map('int', '42'); +// => 42 + +// --- +// List type will accept non-incremental keys. + +$flexibleMapper->map('list', ['foo' => 42, 'bar' => 1337]); +// => [0 => 42, 1 => 1338] -If the limitations are too restrictive, the mapper can be made more flexible to -disable all strict rules declared above and enable value casting when possible. +// --- +// If a value is missing in a source for a node that accepts `null`, the +// node will be filled with `null`. + +$flexibleMapper->map( + 'array{foo: string, bar: null|string}', + ['foo' => 'foo'] // `bar` is missing +); +// => ['foo' => 'foo', 'bar' => null] + +// --- +// Array and list types will convert `null` or missing values to an empty +// array. + +$flexibleMapper->map( + 'array{foo: string, bar: array}', + ['foo' => 'foo'] // `bar` is missing +); +// => ['foo' => 'foo', 'bar' => []] +``` + +## Allowing superfluous keys + +With this setting enabled, superfluous keys in source arrays will be allowed, +preventing errors when a value is not bound to any object property/parameter or +shaped array element. ```php (new \CuyZ\Valinor\MapperBuilder()) - ->flexible() + ->allowSuperfluousKeys() ->mapper() - ->map('array{foo: int, bar: bool}', [ - 'foo' => '42', // The value will be cast from `string` to `int` - 'bar' => 'true', // The value will be cast from `string` to `bool` - 'baz' => '…', // Will be ignored - ]); + ->map( + 'array{foo: string, bar: int}', + [ + 'foo' => 'foo', + 'bar' => 42, + 'baz' => 1337.404, // `baz` will be ignored + ] + ); ``` -## When should flexible mode be enabled? +## Allowing permissive types -When using this library for a provider application — for instance an API -endpoint that can be called with a JSON payload — it is recommended to use the -strict mode. This ensures that the consumers of the API provide the exact -awaited data structure, and prevents unknown values to be passed. +This setting allows permissive types `mixed` and `object` to be used during +mapping. -When using this library as a consumer of an external source, it can make sense -to enable the flexible mode. This allows for instance to convert string numeric -values to integers or to ignore data that is present in the source but not -needed in the application. +```php +(new \CuyZ\Valinor\MapperBuilder()) + ->allowPermissiveTypes() + ->mapper() + ->map( + 'array{foo: string, bar: mixed}', + [ + 'foo' => 'foo', + 'bar' => 42, // Could be any value + ] + ); +``` diff --git a/src/Library/Container.php b/src/Library/Container.php index 346aa509..85cf6036 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -87,8 +87,8 @@ public function __construct(Settings $settings) ShellVisitor::class => fn () => new AttributeShellVisitor(), NodeBuilder::class => function () use ($settings) { - $listNodeBuilder = new ListNodeBuilder($settings->flexible); - $arrayNodeBuilder = new ArrayNodeBuilder($settings->flexible); + $listNodeBuilder = new ListNodeBuilder($settings->enableFlexibleCasting); + $arrayNodeBuilder = new ArrayNodeBuilder($settings->enableFlexibleCasting); $builder = new CasterNodeBuilder([ ListType::class => $listNodeBuilder, @@ -96,17 +96,18 @@ public function __construct(Settings $settings) ArrayType::class => $arrayNodeBuilder, NonEmptyArrayType::class => $arrayNodeBuilder, IterableType::class => $arrayNodeBuilder, - ShapedArrayType::class => new ShapedArrayNodeBuilder($settings->flexible), - ScalarType::class => new ScalarNodeBuilder($settings->flexible), + ShapedArrayType::class => new ShapedArrayNodeBuilder($settings->allowSuperfluousKeys), + ScalarType::class => new ScalarNodeBuilder($settings->enableFlexibleCasting), ]); - $builder = new UnionNodeBuilder($builder, $settings->flexible); + $builder = new UnionNodeBuilder($builder, $settings->enableFlexibleCasting); $builder = new ClassNodeBuilder( $builder, $this->get(ClassDefinitionRepository::class), $this->get(ObjectBuilderFactory::class), - $settings->flexible + $settings->enableFlexibleCasting, + $settings->allowSuperfluousKeys ); $builder = new InterfaceNodeBuilder( @@ -114,7 +115,7 @@ public function __construct(Settings $settings) $this->get(ObjectImplementations::class), $this->get(ClassDefinitionRepository::class), $this->get(ObjectBuilderFactory::class), - $settings->flexible + $settings->enableFlexibleCasting ); $builder = new CasterProxyNodeBuilder($builder); @@ -126,7 +127,7 @@ public function __construct(Settings $settings) $settings->valueModifier ) ); - $builder = new StrictNodeBuilder($builder, $settings->flexible); + $builder = new StrictNodeBuilder($builder, $settings->allowPermissiveTypes, $settings->enableFlexibleCasting); $builder = new ShellVisitorNodeBuilder($builder, $this->get(ShellVisitor::class)); return new ErrorCatcherNodeBuilder($builder, $settings->exceptionFilter); @@ -152,7 +153,7 @@ public function __construct(Settings $settings) $factory = new AttributeObjectBuilderFactory($factory); $factory = new CollisionObjectBuilderFactory($factory); - if (! $settings->flexible) { + if (! $settings->allowPermissiveTypes) { $factory = new StrictTypesObjectBuilderFactory($factory); } diff --git a/src/Library/Settings.php b/src/Library/Settings.php index 7f29dfc4..23851859 100644 --- a/src/Library/Settings.php +++ b/src/Library/Settings.php @@ -28,7 +28,11 @@ final class Settings /** @var CacheInterface */ public CacheInterface $cache; - public bool $flexible = false; + public bool $enableFlexibleCasting = false; + + public bool $allowSuperfluousKeys = false; + + public bool $allowPermissiveTypes = false; /** @var callable(Throwable): ErrorMessage */ public $exceptionFilter; diff --git a/src/Mapper/Object/FilledArguments.php b/src/Mapper/Object/FilledArguments.php index 1b69219b..912af2fb 100644 --- a/src/Mapper/Object/FilledArguments.php +++ b/src/Mapper/Object/FilledArguments.php @@ -28,18 +28,15 @@ final class FilledArguments implements IteratorAggregate private Arguments $arguments; - private bool $flexible; - - private function __construct(Arguments $arguments, Shell $shell, bool $flexible) + private function __construct(Arguments $arguments, Shell $shell) { $this->arguments = $arguments; - $this->flexible = $flexible; $this->hasValue = $shell->hasValue(); } - public static function forInterface(Arguments $arguments, Shell $shell, bool $flexible): self + public static function forInterface(Arguments $arguments, Shell $shell): self { - $self = new self($arguments, $shell, $flexible); + $self = new self($arguments, $shell); if ($self->hasValue) { if (count($arguments) > 0) { @@ -50,9 +47,9 @@ public static function forInterface(Arguments $arguments, Shell $shell, bool $fl return $self; } - public static function forClass(Arguments $arguments, Shell $shell, bool $flexible): self + public static function forClass(Arguments $arguments, Shell $shell): self { - $self = new self($arguments, $shell, $flexible); + $self = new self($arguments, $shell); if ($self->hasValue) { $self->value = $self->transform($shell->value()); @@ -109,10 +106,6 @@ private function transform($source): array } } - if ($argumentsCount === 0 && $this->flexible && ! $isArray) { - return []; - } - if (! $isArray) { throw new SourceIsNotAnArray($source, $this->arguments); } diff --git a/src/Mapper/Tree/Builder/ArrayNodeBuilder.php b/src/Mapper/Tree/Builder/ArrayNodeBuilder.php index 87a32da4..0506c923 100644 --- a/src/Mapper/Tree/Builder/ArrayNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ArrayNodeBuilder.php @@ -18,11 +18,11 @@ /** @internal */ final class ArrayNodeBuilder implements NodeBuilder { - private bool $flexible; + private bool $enableFlexibleCasting; - public function __construct(bool $flexible) + public function __construct(bool $enableFlexibleCasting) { - $this->flexible = $flexible; + $this->enableFlexibleCasting = $enableFlexibleCasting; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -32,7 +32,7 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode assert($type instanceof ArrayType || $type instanceof NonEmptyArrayType || $type instanceof IterableType); - if (null === $value && $this->flexible) { + if ($this->enableFlexibleCasting && $value === null) { return TreeNode::branch($shell, [], []); } diff --git a/src/Mapper/Tree/Builder/ClassNodeBuilder.php b/src/Mapper/Tree/Builder/ClassNodeBuilder.php index d8385bed..58eabac9 100644 --- a/src/Mapper/Tree/Builder/ClassNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ClassNodeBuilder.php @@ -26,18 +26,22 @@ final class ClassNodeBuilder implements NodeBuilder private ObjectBuilderFactory $objectBuilderFactory; - private bool $flexible; + private bool $enableFlexibleCasting; + + private bool $allowSuperfluousKeys; public function __construct( NodeBuilder $delegate, ClassDefinitionRepository $classDefinitionRepository, ObjectBuilderFactory $objectBuilderFactory, - bool $flexible + bool $enableFlexibleCasting, + bool $allowSuperfluousKeys ) { $this->delegate = $delegate; $this->classDefinitionRepository = $classDefinitionRepository; $this->objectBuilderFactory = $objectBuilderFactory; - $this->flexible = $flexible; + $this->enableFlexibleCasting = $enableFlexibleCasting; + $this->allowSuperfluousKeys = $allowSuperfluousKeys; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -48,8 +52,12 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode return $this->delegate->build($shell, $rootBuilder); } + if ($this->enableFlexibleCasting && $shell->value() === null) { + $shell = $shell->withValue([]); + } + $builder = $this->builder($shell, ...$classTypes); - $arguments = FilledArguments::forClass($builder->describeArguments(), $shell, $this->flexible); + $arguments = FilledArguments::forClass($builder->describeArguments(), $shell); $children = []; @@ -71,8 +79,8 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode $node = TreeNode::branch($shell, $object, $children); - if (! $this->flexible) { - $node = $this->checkForUnexpectedKeys($arguments, $node); + if (! $this->allowSuperfluousKeys) { + $node = $this->checkForSuperfluousKeys($arguments, $node); } return $node; @@ -125,7 +133,7 @@ private function buildObject(ObjectBuilder $builder, array $children): ?object return $builder->build($arguments); } - private function checkForUnexpectedKeys(FilledArguments $arguments, TreeNode $node): TreeNode + private function checkForSuperfluousKeys(FilledArguments $arguments, TreeNode $node): TreeNode { $superfluousKeys = $arguments->superfluousKeys(); diff --git a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php index 11d2df56..a0b00056 100644 --- a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php +++ b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php @@ -27,20 +27,20 @@ final class InterfaceNodeBuilder implements NodeBuilder private ObjectBuilderFactory $objectBuilderFactory; - private bool $flexible; + private bool $enableFlexibleCasting; public function __construct( NodeBuilder $delegate, ObjectImplementations $implementations, ClassDefinitionRepository $classDefinitionRepository, ObjectBuilderFactory $objectBuilderFactory, - bool $flexible + bool $enableFlexibleCasting ) { $this->delegate = $delegate; $this->implementations = $implementations; $this->classDefinitionRepository = $classDefinitionRepository; $this->objectBuilderFactory = $objectBuilderFactory; - $this->flexible = $flexible; + $this->enableFlexibleCasting = $enableFlexibleCasting; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -51,11 +51,15 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode return $this->delegate->build($shell, $rootBuilder); } + if ($this->enableFlexibleCasting && $shell->value() === null) { + $shell = $shell->withValue([]); + } + $interfaceName = $type->className(); $function = $this->implementations->function($interfaceName); $arguments = Arguments::fromParameters($function->parameters()); - $arguments = FilledArguments::forInterface($arguments, $shell, $this->flexible); + $arguments = FilledArguments::forInterface($arguments, $shell); $children = $this->children($shell, $arguments, $rootBuilder); $values = []; diff --git a/src/Mapper/Tree/Builder/ListNodeBuilder.php b/src/Mapper/Tree/Builder/ListNodeBuilder.php index d37a4bc6..81e080f8 100644 --- a/src/Mapper/Tree/Builder/ListNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ListNodeBuilder.php @@ -17,11 +17,11 @@ /** @internal */ final class ListNodeBuilder implements NodeBuilder { - private bool $flexible; + private bool $enableFlexibleCasting; - public function __construct(bool $flexible) + public function __construct(bool $enableFlexibleCasting) { - $this->flexible = $flexible; + $this->enableFlexibleCasting = $enableFlexibleCasting; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -31,7 +31,7 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode assert($type instanceof ListType || $type instanceof NonEmptyListType); - if (null === $value && $this->flexible) { + if ($this->enableFlexibleCasting && $value === null) { return TreeNode::branch($shell, [], []); } @@ -58,7 +58,7 @@ private function children(CompositeTraversableType $type, Shell $shell, RootNode $children = []; foreach ($values as $key => $value) { - if ($this->flexible || $key === $expected) { + if ($this->enableFlexibleCasting || $key === $expected) { $child = $shell->child((string)$expected, $subType); $children[$expected] = $rootBuilder->build($child->withValue($value)); } else { diff --git a/src/Mapper/Tree/Builder/ScalarNodeBuilder.php b/src/Mapper/Tree/Builder/ScalarNodeBuilder.php index 6ccb2db4..6ae646f6 100644 --- a/src/Mapper/Tree/Builder/ScalarNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ScalarNodeBuilder.php @@ -13,11 +13,11 @@ /** @internal */ final class ScalarNodeBuilder implements NodeBuilder { - private bool $flexible; + private bool $enableFlexibleCasting; - public function __construct(bool $flexible) + public function __construct(bool $enableFlexibleCasting) { - $this->flexible = $flexible; + $this->enableFlexibleCasting = $enableFlexibleCasting; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -30,7 +30,7 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode // The flexible mode is always active for enum types, as it makes no // sense not to activate it in the strict mode: a scalar value is always // wanted as input. - if ((! $this->flexible && ! $type instanceof EnumType) || ! $type->canCast($value)) { + if ((! $this->enableFlexibleCasting && ! $type instanceof EnumType) || ! $type->canCast($value)) { throw $type->errorMessage(); } diff --git a/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php b/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php index a64134af..60aa241c 100644 --- a/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php @@ -18,11 +18,11 @@ /** @internal */ final class ShapedArrayNodeBuilder implements NodeBuilder { - private bool $flexible; + private bool $allowSuperfluousKeys; - public function __construct(bool $flexible) + public function __construct(bool $allowSuperfluousKeys) { - $this->flexible = $flexible; + $this->allowSuperfluousKeys = $allowSuperfluousKeys; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -69,7 +69,7 @@ private function children(ShapedArrayType $type, Shell $shell, RootNodeBuilder $ unset($value[$key]); } - if (! $this->flexible && count($value) > 0) { + if (! $this->allowSuperfluousKeys && count($value) > 0) { throw new UnexpectedShapedArrayKeys(array_keys($value), $elements); } diff --git a/src/Mapper/Tree/Builder/StrictNodeBuilder.php b/src/Mapper/Tree/Builder/StrictNodeBuilder.php index 52a55949..5c62a9fa 100644 --- a/src/Mapper/Tree/Builder/StrictNodeBuilder.php +++ b/src/Mapper/Tree/Builder/StrictNodeBuilder.php @@ -13,24 +13,27 @@ final class StrictNodeBuilder implements NodeBuilder { private NodeBuilder $delegate; - private bool $flexible; + private bool $allowPermissiveTypes; - public function __construct(NodeBuilder $delegate, bool $flexible) + private bool $enableFlexibleCasting; + + public function __construct(NodeBuilder $delegate, bool $allowPermissiveTypes, bool $enableFlexibleCasting) { $this->delegate = $delegate; - $this->flexible = $flexible; + $this->allowPermissiveTypes = $allowPermissiveTypes; + $this->enableFlexibleCasting = $enableFlexibleCasting; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode { $type = $shell->type(); - if (! $this->flexible) { + if (! $this->allowPermissiveTypes) { TypeHelper::checkPermissiveType($type); } if (! $shell->hasValue()) { - if ($this->flexible) { + if ($this->enableFlexibleCasting) { return $this->delegate->build($shell->withValue(null), $rootBuilder); } diff --git a/src/Mapper/Tree/Builder/UnionNodeBuilder.php b/src/Mapper/Tree/Builder/UnionNodeBuilder.php index 89651b83..932ce843 100644 --- a/src/Mapper/Tree/Builder/UnionNodeBuilder.php +++ b/src/Mapper/Tree/Builder/UnionNodeBuilder.php @@ -19,12 +19,12 @@ final class UnionNodeBuilder implements NodeBuilder { private NodeBuilder $delegate; - private bool $flexible; + private bool $enableFlexibleCasting; - public function __construct(NodeBuilder $delegate, bool $flexible) + public function __construct(NodeBuilder $delegate, bool $enableFlexibleCasting) { $this->delegate = $delegate; - $this->flexible = $flexible; + $this->enableFlexibleCasting = $enableFlexibleCasting; } public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -60,7 +60,7 @@ private function narrow(UnionType $type, $source): Type continue; } - if (! $this->flexible && ! $subType instanceof EnumType) { + if (! $this->enableFlexibleCasting && ! $subType instanceof EnumType) { continue; } diff --git a/src/MapperBuilder.php b/src/MapperBuilder.php index f308e2bc..6c657d6c 100644 --- a/src/MapperBuilder.php +++ b/src/MapperBuilder.php @@ -248,25 +248,118 @@ public function alter(callable $callback): self } /** - * Enables flexible mode for the mapper: - * - * - Scalar types will be cast whenever possible, for instance an integer - * node will accept any valid numeric value like the string "42". + * @deprecated use the following method(s) depending on your needs: + * @see \CuyZ\Valinor\MapperBuilder::enableFlexibleCasting() + * @see \CuyZ\Valinor\MapperBuilder::allowSuperfluousKeys() + * @see \CuyZ\Valinor\MapperBuilder::allowPermissiveTypes() + */ + public function flexible(): self + { + $clone = clone $this; + $clone->settings->enableFlexibleCasting = true; + $clone->settings->allowSuperfluousKeys = true; + $clone->settings->allowPermissiveTypes = true; + + return $clone; + } + + /** + * Changes the behaviours explained below: * - * - Superfluous keys in source arrays will be ignored, preventing errors - * when a value is not bound to any object property/parameter or shaped - * array element. + * ```php + * $flexibleMapper = (new \CuyZ\Valinor\MapperBuilder()) + * ->enableFlexibleCasting() + * ->mapper(); + * + * // --- + * // Scalar types will accept non-strict values; for instance an integer + * // type will accept any valid numeric value like the *string* "42". + * + * $flexibleMapper->map('int', '42'); + * // => 42 + * + * // --- + * // List type will accept non-incremental keys. + * + * $flexibleMapper->map('list', ['foo' => 42, 'bar' => 1337]); + * // => [0 => 42, 1 => 1338] + * + * // --- + * // If a value is missing in a source for a node that accepts `null`, the + * // node will be filled with `null`. + * + * $flexibleMapper->map( + * 'array{foo: string, bar: null|string}', + * ['foo' => 'foo'] // `bar` is missing + * ); + * // => ['foo' => 'foo', 'bar' => null] + * + * // --- + * // Array and list types will convert `null` or missing values to an empty + * // array. + * + * $flexibleMapper->map( + * 'array{foo: string, bar: array}', + * ['foo' => 'foo'] // `bar` is missing + * ); + * // => ['foo' => 'foo', 'bar' => []] + * ``` + */ + public function enableFlexibleCasting(): self + { + $clone = clone $this; + $clone->settings->enableFlexibleCasting = true; + + return $clone; + } + + /** + * Superfluous keys in source arrays will be ignored, preventing errors when + * a value is not bound to any object property/parameter or shaped array + * element. * - * - Permissive types `mixed` and `object` are allowed. + * ```php + * (new \CuyZ\Valinor\MapperBuilder()) + * ->allowSuperfluousKeys() + * ->mapper() + * ->map( + * 'array{foo: string, bar: int}', + * [ + * 'foo' => 'foo', + * 'bar' => 42, + * 'baz' => 1337.404, // `baz` will be ignored + * ] + * ); + * ``` */ - public function flexible(): self + public function allowSuperfluousKeys(): self { - if ($this->settings->flexible) { - return $this; - } + $clone = clone $this; + $clone->settings->allowSuperfluousKeys = true; + return $clone; + } + + /** + * Allows permissive types `mixed` and `object` to be used during mapping. + * + * ```php + * (new \CuyZ\Valinor\MapperBuilder()) + * ->allowPermissiveTypes() + * ->mapper() + * ->map( + * 'array{foo: string, bar: mixed}', + * [ + * 'foo' => 'foo', + * 'bar' => 42, // Could be any value + * ] + * ); + * ``` + */ + public function allowPermissiveTypes(): self + { $clone = clone $this; - $clone->settings->flexible = true; + $clone->settings->allowPermissiveTypes = true; return $clone; } diff --git a/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php b/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php new file mode 100644 index 00000000..73e7ac44 --- /dev/null +++ b/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php @@ -0,0 +1,269 @@ +mapper = (new MapperBuilder())->enableFlexibleCasting()->mapper(); + } + + public function test_array_of_scalars_is_mapped_properly(): void + { + $source = ['foo', 42, 1337.404]; + + try { + $result = $this->mapper->map('string[]', $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(['foo', '42', '1337.404'], $result); + } + + public function test_shaped_array_is_mapped_correctly(): void + { + try { + $result = $this->mapper->map( + 'array{string, foo: int, bar?: float}', + [ + 'foo', + 'foo' => '42', + ] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(['foo', 'foo' => 42], $result); + } + + /** + * @requires PHP >= 8.1 + */ + public function test_string_enum_is_cast_correctly(): void + { + try { + $result = $this->mapper->map(BackedStringEnum::class, new StringableObject('foo')); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(BackedStringEnum::FOO, $result); + } + + /** + * @requires PHP >= 8.1 + */ + public function test_integer_enum_is_cast_correctly(): void + { + try { + $result = $this->mapper->map(BackedIntegerEnum::class, '42'); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(BackedIntegerEnum::FOO, $result); + } + + public function test_list_filled_with_associative_array_is_converted_to_list(): void + { + try { + $result = $this->mapper->map( + 'list', + [ + 'foo' => 'foo', + 'bar' => 'bar', + ] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(['foo', 'bar'], $result); + } + + public function test_null_value_for_class_fills_it_with_empty_array(): void + { + try { + $this->mapper->map(stdClass::class, null); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::expectNotToPerformAssertions(); + } + + public function test_null_value_for_interface_with_no_properties_needed_fills_it_with_empty_array(): void + { + try { + $result = (new MapperBuilder()) + ->infer(SomeInterfaceForClassWithNoProperties::class, fn () => SomeClassWithNoProperties::class) + ->enableFlexibleCasting() + ->mapper() + ->map(SomeInterfaceForClassWithNoProperties::class, null); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertInstanceOf(SomeClassWithNoProperties::class, $result); + } + + public function test_interface_is_inferred_and_mapped_properly_in_flexible_casting_mode(): void + { + try { + $result = (new MapperBuilder()) + ->infer(SomeInterfaceForClassWithProperties::class, fn () => SomeClassWithProperties::class) + ->enableFlexibleCasting() + ->mapper() + ->map(SomeInterfaceForClassWithProperties::class, [ + 'foo' => 'foo', + 'bar' => 'bar', + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertInstanceOf(SomeClassWithProperties::class, $result); + self::assertSame('foo', $result->foo); + self::assertSame('bar', $result->bar); + } + + public function test_missing_value_for_array_fills_it_with_empty_array(): void + { + try { + $result = $this->mapper->map( + 'array{foo: string, bar: array}', + ['foo' => 'foo'] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result['foo']); + self::assertSame([], $result['bar']); + } + + public function test_null_value_for_array_fills_it_with_empty_array(): void + { + try { + $result = $this->mapper->map( + 'array{foo: string, bar: array}', + [ + 'foo' => 'foo', + 'bar' => null, + ] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result['foo']); + self::assertSame([], $result['bar']); + } + + public function test_missing_value_for_list_fills_it_with_empty_array(): void + { + try { + $result = $this->mapper->map( + 'array{foo: string, bar: list}', + ['foo' => 'foo'] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result['foo']); + self::assertSame([], $result['bar']); + } + + public function test_null_value_for_list_fills_it_with_empty_array(): void + { + try { + $result = $this->mapper->map( + 'array{foo: string, bar: list}', + [ + 'foo' => 'foo', + 'bar' => null, + ] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result['foo']); + self::assertSame([], $result['bar']); + } + + public function test_missing_value_for_nullable_property_fills_it_with_null(): void + { + $class = new class () { + public string $foo; + + /** @noRector \Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector */ + public ?string $bar; + }; + + try { + $result = $this->mapper->map( + get_class($class), + ['foo' => 'foo'] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result->foo); + self::assertSame(null, $result->bar); + } + + public function test_missing_value_for_nullable_shaped_array_element_fills_it_with_null(): void + { + try { + $result = $this->mapper->map( + 'array{foo: string, bar: ?string}', + ['foo' => 'foo'] + ); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result['foo']); + self::assertSame(null, $result['bar']); + } +} + +interface SomeInterfaceForClassWithNoProperties +{ +} + +final class SomeClassWithNoProperties implements SomeInterfaceForClassWithNoProperties +{ +} + +interface SomeInterfaceForClassWithProperties +{ +} + +final class SomeClassWithProperties implements SomeInterfaceForClassWithProperties +{ + public string $foo; + + public string $bar; +} diff --git a/tests/Integration/Mapping/Other/FlexibleMappingTest.php b/tests/Integration/Mapping/Other/FlexibleMappingTest.php index b46c3ee5..a4b8d86c 100644 --- a/tests/Integration/Mapping/Other/FlexibleMappingTest.php +++ b/tests/Integration/Mapping/Other/FlexibleMappingTest.php @@ -8,7 +8,6 @@ use CuyZ\Valinor\MapperBuilder; use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum; use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum; -use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum; use CuyZ\Valinor\Tests\Fixture\Object\StringableObject; use CuyZ\Valinor\Tests\Integration\IntegrationTest; use DateTime; @@ -240,205 +239,6 @@ public function test_missing_value_for_nullable_shaped_array_element_fills_it_wi self::assertSame('foo', $result['foo']); self::assertSame(null, $result['bar']); } - - public function test_value_that_cannot_be_cast_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('int', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame("Value 'foo' is not a valid integer.", (string)$error); - } - } - - public function test_null_that_cannot_be_cast_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('int', null); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('Value null is not a valid integer.', (string)$error); - } - } - - public function test_invalid_float_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('float', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame("Value 'foo' is not a valid float.", (string)$error); - } - } - - public function test_invalid_float_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('42.404', 1337); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('Value 1337 does not match float value 42.404.', (string)$error); - } - } - - public function test_invalid_integer_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('42', 1337); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('Value 1337 does not match integer value 42.', (string)$error); - } - } - - public function test_invalid_integer_range_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('int<42, 1337>', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame("Value 'foo' is not a valid integer between 42 and 1337.", (string)$error); - } - } - - /** - * @requires PHP >= 8.1 - */ - public function test_invalid_enum_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map(PureEnum::class, 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame("Value 'foo' does not match any of 'FOO', 'BAR', 'BAZ'.", (string)$error); - } - } - - /** - * @requires PHP >= 8.1 - */ - public function test_invalid_string_enum_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map(BackedStringEnum::class, new stdClass()); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame("Value object(stdClass) does not match any of 'foo', 'bar', 'baz'.", (string)$error); - } - } - - /** - * @requires PHP >= 8.1 - */ - public function test_invalid_integer_enum_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map(BackedIntegerEnum::class, false); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame("Value false does not match any of 42, 404, 1337.", (string)$error); - } - } - - public function test_invalid_union_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('bool|int|float', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('1607027306', $error->code()); - self::assertSame("Value 'foo' does not match any of `bool`, `int`, `float`.", (string)$error); - } - } - - public function test_null_in_union_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('bool|int|float', null); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('1607027306', $error->code()); - self::assertSame('Cannot be empty and must be filled with a value matching any of `bool`, `int`, `float`.', (string)$error); - } - } - - public function test_invalid_array_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('array', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('1618739163', $error->code()); - self::assertSame("Value 'foo' does not match type `array`.", (string)$error); - } - } - - public function test_invalid_list_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('list', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('1618739163', $error->code()); - self::assertSame("Value 'foo' does not match type `list`.", (string)$error); - } - } - - public function test_invalid_shaped_array_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('array{foo: string}', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('1618739163', $error->code()); - self::assertSame("Value 'foo' does not match type `array{foo: string}`.", (string)$error); - } - } - - public function test_invalid_shaped_array_with_object_value_throws_exception(): void - { - try { - (new MapperBuilder())->flexible()->mapper()->map('array{foo: stdClass}', 'foo'); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; - - self::assertSame('1618739163', $error->code()); - self::assertSame("Invalid value 'foo'.", (string)$error); - } - } - - public function test_missing_value_throws_exception(): void - { - $class = new class () { - public string $foo; - - public string $bar; - }; - - try { - (new MapperBuilder())->flexible()->mapper()->map(get_class($class), [ - 'foo' => 'foo', - ]); - } catch (MappingError $exception) { - $error = $exception->node()->children()['bar']->messages()[0]; - - self::assertSame('Value *missing* is not a valid string.', (string)$error); - } - } } // @PHP8.1 Readonly properties diff --git a/tests/Integration/Mapping/Other/PermissiveTypesMappingTest.php b/tests/Integration/Mapping/Other/PermissiveTypesMappingTest.php new file mode 100644 index 00000000..339cdb15 --- /dev/null +++ b/tests/Integration/Mapping/Other/PermissiveTypesMappingTest.php @@ -0,0 +1,51 @@ +mapper = (new MapperBuilder())->allowPermissiveTypes()->mapper(); + } + + public function test_can_map_to_mixed_type(): void + { + try { + $result = $this->mapper->map('mixed[]', [ + 'foo' => 'foo', + 'bar' => 'bar', + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(['foo' => 'foo', 'bar' => 'bar'], $result); + } + + public function test_can_map_to_undefined_object_type(): void + { + $source = [new stdClass(), new DateTime()]; + + try { + $result = $this->mapper->map('object[]', $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame($source, $result); + } +} diff --git a/tests/Integration/Mapping/Other/StrictMappingTest.php b/tests/Integration/Mapping/Other/StrictMappingTest.php index 6cdab025..27581f97 100644 --- a/tests/Integration/Mapping/Other/StrictMappingTest.php +++ b/tests/Integration/Mapping/Other/StrictMappingTest.php @@ -7,7 +7,10 @@ use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\Mapper\Object\Exception\PermissiveTypeNotAllowed; use CuyZ\Valinor\MapperBuilder; +use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum; +use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum; use CuyZ\Valinor\Tests\Fixture\Enum\ClassWithBackedStringEnum; +use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum; use CuyZ\Valinor\Tests\Integration\IntegrationTest; use CuyZ\Valinor\Utility\PermissiveTypeFound; use stdClass; @@ -114,6 +117,186 @@ public function test_superfluous_key_for_single_enum_node_throws_correct_excepti self::assertSame('Cannot be empty and must be filled with a value matching type `foo|bar|baz`.', (string)$errorB); } } + + public function test_null_that_cannot_be_cast_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('int', null); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('Value null is not a valid integer.', (string)$error); + } + } + + public function test_invalid_float_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('float', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame("Value 'foo' is not a valid float.", (string)$error); + } + } + + public function test_invalid_float_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('42.404', 1337); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('Value 1337 does not match float value 42.404.', (string)$error); + } + } + + public function test_invalid_integer_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('int', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame("Value 'foo' is not a valid integer.", (string)$error); + } + } + + public function test_invalid_integer_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('42', 1337); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('Value 1337 does not match integer value 42.', (string)$error); + } + } + + public function test_invalid_integer_range_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('int<42, 1337>', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame("Value 'foo' is not a valid integer between 42 and 1337.", (string)$error); + } + } + + /** + * @requires PHP >= 8.1 + */ + public function test_invalid_enum_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map(PureEnum::class, 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame("Value 'foo' does not match any of 'FOO', 'BAR', 'BAZ'.", (string)$error); + } + } + + /** + * @requires PHP >= 8.1 + */ + public function test_invalid_string_enum_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map(BackedStringEnum::class, new stdClass()); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame("Value object(stdClass) does not match any of 'foo', 'bar', 'baz'.", (string)$error); + } + } + + /** + * @requires PHP >= 8.1 + */ + public function test_invalid_integer_enum_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map(BackedIntegerEnum::class, false); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame("Value false does not match any of 42, 404, 1337.", (string)$error); + } + } + + public function test_invalid_union_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('bool|int|float', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1607027306', $error->code()); + self::assertSame("Value 'foo' does not match any of `bool`, `int`, `float`.", (string)$error); + } + } + + public function test_null_in_union_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('bool|int|float', null); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1607027306', $error->code()); + self::assertSame("Cannot be empty and must be filled with a value matching any of `bool`, `int`, `float`.", (string)$error); + } + } + + public function test_invalid_array_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('array', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1618739163', $error->code()); + self::assertSame("Value 'foo' does not match type `array`.", (string)$error); + } + } + + public function test_invalid_list_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('list', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1618739163', $error->code()); + self::assertSame("Value 'foo' does not match type `list`.", (string)$error); + } + } + + public function test_invalid_shaped_array_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('array{foo: string}', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1618739163', $error->code()); + self::assertSame("Value 'foo' does not match type `array{foo: string}`.", (string)$error); + } + } + + public function test_invalid_shaped_array_with_object_value_throws_exception(): void + { + try { + (new MapperBuilder())->mapper()->map('array{foo: stdClass}', 'foo'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1618739163', $error->code()); + self::assertSame("Invalid value 'foo'.", (string)$error); + } + } } final class ObjectContainingUndefinedObjectType diff --git a/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php b/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php new file mode 100644 index 00000000..aafcf11f --- /dev/null +++ b/tests/Integration/Mapping/Other/SuperfluousKeysMappingTest.php @@ -0,0 +1,58 @@ +mapper = (new MapperBuilder())->allowSuperfluousKeys()->mapper(); + } + + public function test_superfluous_shaped_array_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->mapper->map($signature, $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(42, $result['bar']); + } + } + + public function test_source_matching_two_unions_maps_the_one_with_most_arguments(): void + { + try { + $result = $this->mapper->map(UnionOfBarAndFizAndFoo::class, [ + ['foo' => 'foo', 'bar' => 'bar', 'fiz' => 'fiz'], + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + $object = $result->objects[0]; + + self::assertInstanceOf(SomeBarAndFizObject::class, $object); + self::assertSame('bar', $object->bar); + self::assertSame('fiz', $object->fiz); + } +} diff --git a/tests/Unit/MapperBuilderTest.php b/tests/Unit/MapperBuilderTest.php index 8ea96614..db5b5216 100644 --- a/tests/Unit/MapperBuilderTest.php +++ b/tests/Unit/MapperBuilderTest.php @@ -33,10 +33,13 @@ public function test_builder_methods_return_clone_of_builder_instance(): void $builderD = $builderA->registerConstructor(static fn (): stdClass => new stdClass()); $builderE = $builderA->alter(static fn (string $value): string => 'foo'); $builderF = $builderA->flexible(); - $builderG = $builderA->filterExceptions(fn () => new FakeErrorMessage()); - $builderH = $builderA->withCache(new FakeCache()); - $builderI = $builderA->withCacheDir(sys_get_temp_dir()); - $builderJ = $builderA->enableLegacyDoctrineAnnotations(); + $builderG = $builderA->enableFlexibleCasting(); + $builderH = $builderA->allowSuperfluousKeys(); + $builderI = $builderA->allowPermissiveTypes(); + $builderJ = $builderA->filterExceptions(fn () => new FakeErrorMessage()); + $builderK = $builderA->withCache(new FakeCache()); + $builderL = $builderA->withCacheDir(sys_get_temp_dir()); + $builderM = $builderA->enableLegacyDoctrineAnnotations(); self::assertNotSame($builderA, $builderB); self::assertNotSame($builderA, $builderC); @@ -47,6 +50,9 @@ public function test_builder_methods_return_clone_of_builder_instance(): void self::assertNotSame($builderA, $builderH); self::assertNotSame($builderA, $builderI); self::assertNotSame($builderA, $builderJ); + self::assertNotSame($builderA, $builderK); + self::assertNotSame($builderA, $builderL); + self::assertNotSame($builderA, $builderM); } public function test_mapper_instance_is_the_same(): void