diff --git a/README.md b/README.md index 4c97987..112a78d 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,11 @@ You can write your own validators if you need more. To use Input Mapper, write a class with a public constructor and add either native or PHPDoc types to all constructor parameters. -Optional fields need to be wrapped with the Optional class, which allows distinguishing between null and missing values. +Optional fields can either be marked with `#[Optional]` attribute (allowing you to specify a default value), +or if you need to distinguish between default and missing values, you can wrap the type with `ShipMonk\InputMapper\Runtime\Optional` class. ```php -use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Compiler\Mapper\Optional; class Person { @@ -78,14 +79,15 @@ class Person public readonly int $age, - /** @var Optional */ - public readonly Optional $email, + #[Optional] + public readonly ?string $email, /** @var list */ public readonly array $hobbies, - /** @var Optional> */ - public readonly Optional $friends, + /** @var list */ + #[Optional(default: [])] + public readonly array $friends, ) {} } ``` diff --git a/src/Compiler/Exception/CannotCreateMapperCompilerException.php b/src/Compiler/Exception/CannotCreateMapperCompilerException.php index 2cf08f5..246afe3 100644 --- a/src/Compiler/Exception/CannotCreateMapperCompilerException.php +++ b/src/Compiler/Exception/CannotCreateMapperCompilerException.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionParameter; use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler; +use ShipMonk\InputMapper\Compiler\Mapper\UndefinedAwareMapperCompiler; use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler; use Throwable; @@ -37,6 +38,25 @@ public static function withIncompatibleMapperForMethodParameter( return new self("Cannot use mapper {$mapperCompilerClass} for parameter \${$parameterName} of method {$methodFullName}, because {$reason}", 0, $previous); } + public static function withIncompatibleDefaultValueParameter( + UndefinedAwareMapperCompiler $mapperCompiler, + ReflectionParameter $parameter, + TypeNode $parameterType, + ?Throwable $previous = null + ): self + { + $mapperCompilerClass = $mapperCompiler::class; + $defaultValueType = $mapperCompiler->getDefaultValueType(); + + $parameterName = $parameter->getName(); + $className = $parameter->getDeclaringClass()?->getName(); + $methodName = $parameter->getDeclaringFunction()->getName(); + $methodFullName = $className !== null ? "{$className}::{$methodName}" : $methodName; + + $reason = "default value of type '{$defaultValueType}' is not compatible with parameter type '{$parameterType}'"; + return new self("Cannot use mapper {$mapperCompilerClass} for parameter \${$parameterName} of method {$methodFullName}, because {$reason}", 0, $previous); + } + public static function withIncompatibleValidator( ValidatorCompiler $validatorCompiler, MapperCompiler $mapperCompiler, diff --git a/src/Compiler/Mapper/Optional.php b/src/Compiler/Mapper/Optional.php new file mode 100644 index 0000000..0f1b54d --- /dev/null +++ b/src/Compiler/Mapper/Optional.php @@ -0,0 +1,17 @@ +mapperCompiler->compile($value, $path, $builder); + } + + public function compileUndefined(Expr $path, Expr $key, PhpCodeBuilder $builder): CompiledExpr + { + if ($this->defaultValue === null || is_scalar($this->defaultValue) || is_array($this->defaultValue)) { + return new CompiledExpr($builder->val($this->defaultValue)); + } + + if ($this->defaultValue instanceof BackedEnum) { + return new CompiledExpr($builder->classConstFetch($builder->importClass($this->defaultValue::class), $this->defaultValue->name)); + } + + throw new LogicException('Unsupported default value type: ' . get_debug_type($this->defaultValue)); + } + + public function getInputType(): TypeNode + { + return $this->mapperCompiler->getInputType(); + } + + public function getOutputType(): TypeNode + { + return $this->mapperCompiler->getOutputType(); + } + + public function getDefaultValueType(): TypeNode + { + return PhpDocTypeUtils::fromValue($this->defaultValue); + } + +} diff --git a/src/Compiler/Mapper/Wrapper/MapOptional.php b/src/Compiler/Mapper/Wrapper/MapOptional.php index f16ec09..93300e7 100644 --- a/src/Compiler/Mapper/Wrapper/MapOptional.php +++ b/src/Compiler/Mapper/Wrapper/MapOptional.php @@ -12,6 +12,8 @@ use ShipMonk\InputMapper\Compiler\Mapper\UndefinedAwareMapperCompiler; use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder; use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Runtime\OptionalNone; +use ShipMonk\InputMapper\Runtime\OptionalSome; #[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)] class MapOptional implements UndefinedAwareMapperCompiler @@ -44,9 +46,14 @@ public function getInputType(): TypeNode public function getOutputType(): TypeNode { return new GenericTypeNode( - new IdentifierTypeNode(Optional::class), + new IdentifierTypeNode(OptionalSome::class), [$this->mapperCompiler->getOutputType()], ); } + public function getDefaultValueType(): TypeNode + { + return new IdentifierTypeNode(OptionalNone::class); + } + } diff --git a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php index 3c42c93..d21fa6d 100644 --- a/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php +++ b/src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php @@ -40,11 +40,14 @@ use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDateTimeImmutable; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapEnum; use ShipMonk\InputMapper\Compiler\Mapper\Object\MapObject; +use ShipMonk\InputMapper\Compiler\Mapper\Optional as OptionalAttribute; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapBool; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapFloat; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapInt; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; +use ShipMonk\InputMapper\Compiler\Mapper\UndefinedAwareMapperCompiler; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\ChainMapperCompiler; +use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapDefaultValue; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapOptional; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\ValidatedMapperCompiler; @@ -74,6 +77,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory final public const DELEGATE_OBJECT_MAPPING = 'delegateObjectMapping'; final public const GENERIC_PARAMETERS = 'genericParameters'; + final public const DEFAULT_VALUE = 'defaultValue'; /** * @param array): MapperCompiler> $mapperCompilerFactories @@ -213,7 +217,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler } if ($isNullable && count($subTypesWithoutNull) === 1) { - return new MapNullable($this->createInner($subTypesWithoutNull[0], $options)); + return $this->create(new NullableTypeNode($subTypesWithoutNull[0]), $options); } } @@ -396,10 +400,20 @@ protected function createParameterMapperCompiler( default => new ChainMapperCompiler($mappers), }; + foreach ($parameterReflection->getAttributes(OptionalAttribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $mapper = new MapDefaultValue($mapper, $attribute->newInstance()->default); + } + if (!PhpDocTypeUtils::isSubTypeOf($mapper->getOutputType(), $type)) { throw CannotCreateMapperCompilerException::withIncompatibleMapperForMethodParameter($mapper, $parameterReflection, $type); } + if ($mapper instanceof UndefinedAwareMapperCompiler) { + if (!PhpDocTypeUtils::isSubTypeOf($mapper->getDefaultValueType(), $type)) { + throw CannotCreateMapperCompilerException::withIncompatibleDefaultValueParameter($mapper, $parameterReflection, $type); + } + } + foreach ($validators as $validator) { $mapper = $this->addValidator($mapper, $validator); } @@ -419,6 +433,10 @@ protected function addValidator( return new ValidatedMapperCompiler($mapperCompiler, [$validatorCompiler]); } + if ($mapperCompiler instanceof MapDefaultValue) { + return new MapDefaultValue($this->addValidator($mapperCompiler->mapperCompiler, $validatorCompiler), $mapperCompiler->defaultValue); + } + if ($mapperCompiler instanceof MapNullable) { return new MapNullable($this->addValidator($mapperCompiler->innerMapperCompiler, $validatorCompiler)); } diff --git a/src/Compiler/Type/PhpDocTypeUtils.php b/src/Compiler/Type/PhpDocTypeUtils.php index 8063738..c71f792 100644 --- a/src/Compiler/Type/PhpDocTypeUtils.php +++ b/src/Compiler/Type/PhpDocTypeUtils.php @@ -46,6 +46,7 @@ use ReflectionUnionType; use Traversable; use function array_flip; +use function array_is_list; use function array_keys; use function array_map; use function array_shift; @@ -54,6 +55,7 @@ use function class_exists; use function constant; use function count; +use function get_debug_type; use function get_object_vars; use function in_array; use function interface_exists; @@ -63,6 +65,8 @@ use function is_float; use function is_int; use function is_object; +use function is_resource; +use function is_scalar; use function is_string; use function max; use function method_exists; @@ -151,6 +155,44 @@ public static function fromReflectionType(ReflectionType $reflectionType): TypeN return new IdentifierTypeNode('mixed'); } + public static function fromValue(mixed $value): TypeNode + { + if (is_scalar($value) || $value === null) { + return new IdentifierTypeNode(get_debug_type($value)); + } + + if (is_array($value)) { + $items = []; + $isList = array_is_list($value); + + foreach ($value as $k => $v) { + $keyName = match (true) { + $isList => null, + is_int($k) => new ConstExprIntegerNode((string) $k), + is_string($k) => new IdentifierTypeNode($k), + }; + + $items[] = new ArrayShapeItemNode( + keyName: $keyName, + optional: false, + valueType: self::fromValue($v), + ); + } + + return new ArrayShapeNode($items); + } + + if (is_object($value)) { + return new IdentifierTypeNode($value::class); + } + + if (is_resource($value)) { + return new IdentifierTypeNode('resource'); + } + + throw new LogicException('Unsupported value type'); + } + /** * @param list $genericParameters */ diff --git a/tests/Compiler/Mapper/Object/Data/DelegateToPerson__PersonInputMapper.php b/tests/Compiler/Mapper/Object/Data/DelegateToPerson__PersonInputMapper.php index 2f70322..c319021 100644 --- a/tests/Compiler/Mapper/Object/Data/DelegateToPerson__PersonInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/DelegateToPerson__PersonInputMapper.php @@ -7,6 +7,7 @@ use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Runtime\OptionalSome; use function array_diff_key; use function array_key_exists; use function array_keys; @@ -86,10 +87,10 @@ private function mapName(mixed $data, array $path = []): string /** * @param list $path - * @return Optional + * @return OptionalSome * @throws MappingFailedException */ - private function mapAge(mixed $data, array $path = []): Optional + private function mapAge(mixed $data, array $path = []): OptionalSome { if (!is_int($data)) { throw MappingFailedException::incorrectType($data, $path, 'int'); diff --git a/tests/Compiler/Mapper/Object/Data/MovieMapper.php b/tests/Compiler/Mapper/Object/Data/MovieMapper.php index 74b45d1..afc2469 100644 --- a/tests/Compiler/Mapper/Object/Data/MovieMapper.php +++ b/tests/Compiler/Mapper/Object/Data/MovieMapper.php @@ -7,6 +7,7 @@ use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Runtime\OptionalSome; use function array_diff_key; use function array_is_list; use function array_key_exists; @@ -107,10 +108,10 @@ private function mapTitle(mixed $data, array $path = []): string /** * @param list $path - * @return Optional + * @return OptionalSome * @throws MappingFailedException */ - private function mapDescription(mixed $data, array $path = []): Optional + private function mapDescription(mixed $data, array $path = []): OptionalSome { if (!is_string($data)) { throw MappingFailedException::incorrectType($data, $path, 'string'); diff --git a/tests/Compiler/Mapper/Object/Data/Movie__PersonInputMapper.php b/tests/Compiler/Mapper/Object/Data/Movie__PersonInputMapper.php index 29feb0f..ecd8331 100644 --- a/tests/Compiler/Mapper/Object/Data/Movie__PersonInputMapper.php +++ b/tests/Compiler/Mapper/Object/Data/Movie__PersonInputMapper.php @@ -7,6 +7,7 @@ use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Runtime\OptionalSome; use function array_diff_key; use function array_key_exists; use function array_keys; @@ -86,10 +87,10 @@ private function mapName(mixed $data, array $path = []): string /** * @param list $path - * @return Optional + * @return OptionalSome * @throws MappingFailedException */ - private function mapAge(mixed $data, array $path = []): Optional + private function mapAge(mixed $data, array $path = []): OptionalSome { if (!is_int($data)) { throw MappingFailedException::incorrectType($data, $path, 'int'); diff --git a/tests/Compiler/Mapper/Object/Data/PersonWithAllowedExtraPropertiesMapper.php b/tests/Compiler/Mapper/Object/Data/PersonWithAllowedExtraPropertiesMapper.php index 17cf0f1..f789749 100644 --- a/tests/Compiler/Mapper/Object/Data/PersonWithAllowedExtraPropertiesMapper.php +++ b/tests/Compiler/Mapper/Object/Data/PersonWithAllowedExtraPropertiesMapper.php @@ -7,6 +7,7 @@ use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Runtime\OptionalSome; use function array_key_exists; use function is_array; use function is_int; @@ -76,10 +77,10 @@ private function mapName(mixed $data, array $path = []): string /** * @param list $path - * @return Optional + * @return OptionalSome * @throws MappingFailedException */ - private function mapAge(mixed $data, array $path = []): Optional + private function mapAge(mixed $data, array $path = []): OptionalSome { if (!is_int($data)) { throw MappingFailedException::incorrectType($data, $path, 'int'); diff --git a/tests/Compiler/Mapper/Wrapper/Data/IntWithDefaultValueMapper.php b/tests/Compiler/Mapper/Wrapper/Data/IntWithDefaultValueMapper.php new file mode 100644 index 0000000..ebf548b --- /dev/null +++ b/tests/Compiler/Mapper/Wrapper/Data/IntWithDefaultValueMapper.php @@ -0,0 +1,34 @@ + + */ +class IntWithDefaultValueMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } +} diff --git a/tests/Compiler/Mapper/Wrapper/Data/OptionalIntMapper.php b/tests/Compiler/Mapper/Wrapper/Data/OptionalIntMapper.php index cbd7740..a7618f3 100644 --- a/tests/Compiler/Mapper/Wrapper/Data/OptionalIntMapper.php +++ b/tests/Compiler/Mapper/Wrapper/Data/OptionalIntMapper.php @@ -7,12 +7,13 @@ use ShipMonk\InputMapper\Runtime\Mapper; use ShipMonk\InputMapper\Runtime\MapperProvider; use ShipMonk\InputMapper\Runtime\Optional; +use ShipMonk\InputMapper\Runtime\OptionalSome; use function is_int; /** * Generated mapper by {@see MapOptional}. Do not edit directly. * - * @implements Mapper> + * @implements Mapper> */ class OptionalIntMapper implements Mapper { @@ -22,10 +23,10 @@ public function __construct(private readonly MapperProvider $provider) /** * @param list $path - * @return Optional + * @return OptionalSome * @throws MappingFailedException */ - public function map(mixed $data, array $path = []): Optional + public function map(mixed $data, array $path = []): OptionalSome { if (!is_int($data)) { throw MappingFailedException::incorrectType($data, $path, 'int'); diff --git a/tests/Compiler/Mapper/Wrapper/Data/Semaphore.php b/tests/Compiler/Mapper/Wrapper/Data/Semaphore.php new file mode 100644 index 0000000..02f375b --- /dev/null +++ b/tests/Compiler/Mapper/Wrapper/Data/Semaphore.php @@ -0,0 +1,20 @@ + + */ +class SemaphoreMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): Semaphore + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + $knownKeys = ['color' => true, 'manufacturer' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new Semaphore( + array_key_exists('color', $data) ? $this->mapColor($data['color'], [...$path, 'color']) : SemaphoreColorEnum::Green, + array_key_exists('manufacturer', $data) ? $this->mapManufacturer($data['manufacturer'], [...$path, 'manufacturer']) : null, + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapColor(mixed $data, array $path = []): SemaphoreColorEnum + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $enum = SemaphoreColorEnum::tryFrom($data); + + if ($enum === null) { + throw MappingFailedException::incorrectValue($data, $path, 'one of ' . implode(', ', array_column(SemaphoreColorEnum::cases(), 'value'))); + } + + return $enum; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapManufacturer(mixed $data, array $path = []): ?string + { + if ($data === null) { + $mapped = null; + } else { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + $mapped = $data; + } + + return $mapped; + } +} diff --git a/tests/Compiler/Mapper/Wrapper/MapDefaultValueTest.php b/tests/Compiler/Mapper/Wrapper/MapDefaultValueTest.php new file mode 100644 index 0000000..20a532a --- /dev/null +++ b/tests/Compiler/Mapper/Wrapper/MapDefaultValueTest.php @@ -0,0 +1,48 @@ +compileMapper('IntWithDefaultValue', $mapperCompiler); + + self::assertSame(1, $mapper->map(1)); + self::assertSame(2, $mapper->map(2)); + + self::assertException( + MappingFailedException::class, + 'Failed to map data at path /: Expected int, got "1"', + static fn() => $mapper->map('1'), + ); + } + + public function testCompileUndefined(): void + { + $mapperCompiler = new MapObject(Semaphore::class, [ + 'color' => new MapDefaultValue(new MapEnum(SemaphoreColorEnum::class, new MapString()), SemaphoreColorEnum::Green), + 'manufacturer' => new MapDefaultValue(new MapNullable(new MapString()), null), + ]); + + $mapper = $this->compileMapper('Semaphore', $mapperCompiler); + + self::assertEquals(new Semaphore(SemaphoreColorEnum::Green, null), $mapper->map([])); + self::assertEquals(new Semaphore(SemaphoreColorEnum::Red, null), $mapper->map(['color' => 'red'])); + self::assertEquals(new Semaphore(SemaphoreColorEnum::Red, 'Siemens'), $mapper->map(['color' => 'red', 'manufacturer' => 'Siemens'])); + } + +} diff --git a/tests/Compiler/MapperFactory/Data/BrandInputWithDefaultValues.php b/tests/Compiler/MapperFactory/Data/BrandInputWithDefaultValues.php new file mode 100644 index 0000000..bbecb0c --- /dev/null +++ b/tests/Compiler/MapperFactory/Data/BrandInputWithDefaultValues.php @@ -0,0 +1,26 @@ + $founders + */ + public function __construct( + #[Optional(default: 'ShipMonk')] + public readonly string $name, + + #[Optional] + public readonly ?int $foundedIn, + + #[Optional(default: ['Jan Bednář'])] + public readonly array $founders, + ) + { + } + +} diff --git a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php index 90841a1..3b491ba 100644 --- a/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php +++ b/tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php @@ -27,6 +27,7 @@ use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapFloat; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapInt; use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapString; +use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapDefaultValue; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapOptional; use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\ValidatedMapperCompiler; @@ -41,6 +42,7 @@ use ShipMonk\InputMapper\Compiler\Validator\String\AssertStringLength; use ShipMonk\InputMapper\Compiler\Validator\String\AssertUrl; use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\BrandInput; +use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\BrandInputWithDefaultValues; use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\CarFilterInput; use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\CarInput; use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\CarInputWithVarTags; @@ -130,6 +132,28 @@ public static function provideCreateOkData(): iterable ), ]; + yield 'BrandInputWithDefaultValues' => [ + BrandInputWithDefaultValues::class, + [], + new MapObject( + BrandInputWithDefaultValues::class, + [ + 'name' => new MapDefaultValue( + new MapString(), + 'ShipMonk', + ), + 'foundedIn' => new MapDefaultValue( + new MapNullable(new MapInt()), + null, + ), + 'founders' => new MapDefaultValue( + new MapList(new MapString()), + ['Jan Bednář'], + ), + ], + ), + ]; + yield 'CarFilterInput' => [ CarFilterInput::class, [], diff --git a/tests/Compiler/Type/PhpDocTypeUtilsTest.php b/tests/Compiler/Type/PhpDocTypeUtilsTest.php index 271fc23..24dfdd3 100644 --- a/tests/Compiler/Type/PhpDocTypeUtilsTest.php +++ b/tests/Compiler/Type/PhpDocTypeUtilsTest.php @@ -141,6 +141,33 @@ public function testFromReflectionType(): void ); } + #[DataProvider('provideFromValueData')] + public function testFromValue(mixed $value, string $expectedType): void + { + self::assertEquals( + $this->parseType($expectedType), + PhpDocTypeUtils::fromValue($value), + ); + } + + /** + * @return iterable + */ + public static function provideFromValueData(): iterable + { + yield [null, 'null']; + yield [true, 'bool']; + yield [false, 'bool']; + yield [1, 'int']; + yield [1.1, 'float']; + yield ['abc', 'string']; + yield [[], 'array{}']; + yield [[1, 2, 'abc'], 'array{int, int, string}']; + yield [['key' => 'value'], 'array{key: string}']; + yield [['foo' => 'abc', 'bar' => null], 'array{foo: string, bar: null}']; + yield [new DateTimeImmutable(), DateTimeImmutable::class]; + } + /** * @param list $genericParameters */