From cc740e1e0159f863048f328117bea54207e91b99 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 24 Dec 2024 11:48:24 +0100 Subject: [PATCH] [TypeInfo] Add `TypeFactoryTrait::fromValue` method --- src/Symfony/Component/TypeInfo/CHANGELOG.md | 1 + .../TypeInfo/Tests/TypeFactoryTest.php | 57 +++++++++++ .../Component/TypeInfo/TypeFactoryTrait.php | 95 +++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index f8bb3abef81d7..122720c1c3e5e 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `Type::accepts()` method + * Add `TypeFactoryTrait::fromValue()` method 7.2 --- diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php index 60a0ded22c648..d1732671604bb 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php @@ -206,4 +206,61 @@ public function testCreateNullable() Type::nullable(Type::union(Type::int(), Type::string(), Type::null())), ); } + + /** + * @dataProvider createFromValueProvider + */ + public function testCreateFromValue(Type $expected, mixed $value) + { + $this->assertEquals($expected, Type::fromValue($value)); + } + + /** + * @return iterable + */ + public static function createFromValueProvider(): iterable + { + // builtin + yield [Type::null(), null]; + yield [Type::true(), true]; + yield [Type::false(), false]; + yield [Type::int(), 1]; + yield [Type::float(), 1.1]; + yield [Type::string(), 'string']; + yield [Type::callable(), strtoupper(...)]; + yield [Type::resource(), fopen('php://temp', 'r')]; + + // object + yield [Type::object(\DateTimeImmutable::class), new \DateTimeImmutable()]; + yield [Type::object(), new \stdClass()]; + + // collection + $arrayAccess = new class implements \ArrayAccess { + public function offsetExists(mixed $offset): bool + { + return true; + } + + public function offsetGet(mixed $offset): mixed + { + return null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + } + + public function offsetUnset(mixed $offset): void + { + } + }; + + yield [Type::list(Type::int()), [1, 2, 3]]; + yield [Type::dict(Type::bool()), ['a' => true, 'b' => false]]; + yield [Type::array(Type::string()), [1 => 'foo', 'bar' => 'baz']]; + yield [Type::array(Type::nullable(Type::bool()), Type::int()), [1 => true, 2 => null, 3 => false]]; + yield [Type::collection(Type::object(\ArrayIterator::class), Type::mixed(), Type::union(Type::int(), Type::string())), new \ArrayIterator()]; + yield [Type::collection(Type::object(\Generator::class), Type::string(), Type::int()), (fn (): iterable => yield 'string')()]; + yield [Type::collection(Type::object($arrayAccess::class)), $arrayAccess]; + } } diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index d32a97276057c..0afc94d1234f1 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -340,4 +340,99 @@ public static function nullable(Type $type): Type return new NullableType($type); } + + public static function fromValue(mixed $value): Type + { + $type = match ($value) { + null => self::null(), + true => self::true(), + false => self::false(), + default => null, + }; + + if (null !== $type) { + return $type; + } + + if (\is_callable($value)) { + return Type::callable(); + } + + if (\is_resource($value)) { + return Type::resource(); + } + + $type = match (get_debug_type($value)) { + TypeIdentifier::INT->value => self::int(), + TypeIdentifier::FLOAT->value => self::float(), + TypeIdentifier::STRING->value => self::string(), + default => null, + }; + + if (null !== $type) { + return $type; + } + + $type = match (true) { + \is_object($value) => \stdClass::class === $value::class ? self::object() : self::object($value::class), + \is_array($value) => self::builtin(TypeIdentifier::ARRAY), + default => null, + }; + + if (null === $type) { + return Type::mixed(); + } + + if (is_iterable($value)) { + /** @var list|BuiltinType> $keyTypes */ + $keyTypes = []; + + /** @var list $valueTypes */ + $valueTypes = []; + + $i = 0; + + foreach ($value as $k => $v) { + $keyTypes[] = self::fromValue($k); + $keyTypes = array_unique($keyTypes); + + $valueTypes[] = self::fromValue($v); + $valueTypes = array_unique($valueTypes); + } + + if ([] !== $keyTypes) { + $keyTypes = array_values($keyTypes); + $keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0]; + + $valueType = null; + foreach ($valueTypes as &$v) { + if ($v->isIdentifiedBy(TypeIdentifier::MIXED)) { + $valueType = Type::mixed(); + + break; + } + + if ($v->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE)) { + $v = Type::bool(); + } + } + + if (!$valueType) { + $valueTypes = array_values(array_unique($valueTypes)); + $valueType = \count($valueTypes) > 1 ? self::union(...$valueTypes) : $valueTypes[0]; + } + } else { + $keyType = Type::union(Type::int(), Type::string()); + $valueType = Type::mixed(); + } + + return self::collection($type, $valueType, $keyType, \is_array($value) && array_is_list($value)); + } + + if ($value instanceof \ArrayAccess) { + return self::collection($type); + } + + return $type; + } }