<?php
namespace Symfony\Component\TypeInfo;
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\EnumType;
use Symfony\Component\TypeInfo\Type\GenericType;
use Symfony\Component\TypeInfo\Type\IntersectionType;
use Symfony\Component\TypeInfo\Type\NullableType;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\TemplateType;
use Symfony\Component\TypeInfo\Type\UnionType;
trait TypeFactoryTrait
{
public static function builtin(TypeIdentifier|string $identifier): BuiltinType
{
$identifier = \is_string($identifier) ? TypeIdentifier::from($identifier) : $identifier;
return new BuiltinType($identifier);
}
public static function int(): BuiltinType
{
return self::builtin(TypeIdentifier::INT);
}
public static function float(): BuiltinType
{
return self::builtin(TypeIdentifier::FLOAT);
}
public static function string(): BuiltinType
{
return self::builtin(TypeIdentifier::STRING);
}
public static function bool(): BuiltinType
{
return self::builtin(TypeIdentifier::BOOL);
}
public static function resource(): BuiltinType
{
return self::builtin(TypeIdentifier::RESOURCE);
}
public static function false(): BuiltinType
{
return self::builtin(TypeIdentifier::FALSE);
}
public static function true(): BuiltinType
{
return self::builtin(TypeIdentifier::TRUE);
}
public static function callable(): BuiltinType
{
return self::builtin(TypeIdentifier::CALLABLE);
}
public static function mixed(): BuiltinType
{
return self::builtin(TypeIdentifier::MIXED);
}
public static function null(): BuiltinType
{
return self::builtin(TypeIdentifier::NULL);
}
public static function void(): BuiltinType
{
return self::builtin(TypeIdentifier::VOID);
}
public static function never(): BuiltinType
{
return self::builtin(TypeIdentifier::NEVER);
}
public static function collection(BuiltinType|ObjectType|GenericType $type, ?Type $value = null, ?Type $key = null, bool $asList = false): CollectionType
{
if (!$type instanceof GenericType && (null !== $value || null !== $key)) {
$type = self::generic($type, $key ?? self::arrayKey(), $value ?? self::mixed());
}
return new CollectionType($type, $asList);
}
public static function array(?Type $value = null, ?Type $key = null, bool $asList = false): CollectionType
{
return self::collection(self::builtin(TypeIdentifier::ARRAY), $value, $key, $asList);
}
public static function iterable(?Type $value = null, ?Type $key = null): CollectionType
{
return self::collection(self::builtin(TypeIdentifier::ITERABLE), $value, $key);
}
public static function list(?Type $value = null): CollectionType
{
return self::array($value, self::int(), asList: true);
}
public static function dict(?Type $value = null): CollectionType
{
return self::array($value, self::string());
}
public static function arrayShape(array $shape, bool $sealed = true, ?Type $extraKeyType = null, ?Type $extraValueType = null): ArrayShapeType
{
$shape = array_map(static function (array|Type $item): array {
return $item instanceof Type
? ['type' => $item, 'optional' => false]
: ['type' => $item['type'], 'optional' => $item['optional'] ?? false];
}, $shape);
if ($extraKeyType || $extraValueType) {
$sealed = false;
}
$extraKeyType ??= !$sealed ? Type::arrayKey() : null;
$extraValueType ??= !$sealed ? Type::mixed() : null;
return new ArrayShapeType($shape, $extraKeyType, $extraValueType);
}
public static function arrayKey(): UnionType
{
return self::union(self::int(), self::string());
}
public static function object(?string $className = null): BuiltinType|ObjectType
{
return null !== $className ? new ObjectType($className) : new BuiltinType(TypeIdentifier::OBJECT);
}
public static function enum(string $className, ?BuiltinType $backingType = null): EnumType
{
if (is_subclass_of($className, \BackedEnum::class)) {
if (null === $backingType) {
$reflectionBackingType = (new \ReflectionEnum($className))->getBackingType();
$typeIdentifier = TypeIdentifier::INT->value === (string) $reflectionBackingType ? TypeIdentifier::INT : TypeIdentifier::STRING;
$backingType = new BuiltinType($typeIdentifier);
}
return new BackedEnumType($className, $backingType);
}
return new EnumType($className);
}
public static function generic(BuiltinType|ObjectType $mainType, Type ...$variableTypes): GenericType
{
return new GenericType($mainType, ...$variableTypes);
}
public static function template(string $name, ?Type $bound = null): TemplateType
{
return new TemplateType($name, $bound ?? Type::mixed());
}
public static function union(Type ...$types): UnionType
{
$unionTypes = [];
$nullableUnion = false;
$isNullable = fn (Type $type): bool => $type instanceof BuiltinType && TypeIdentifier::NULL === $type->getTypeIdentifier();
foreach ($types as $type) {
if ($type instanceof NullableType) {
$nullableUnion = true;
$type = $type->getWrappedType();
}
if ($type instanceof UnionType) {
foreach ($type->getTypes() as $unionType) {
if ($isNullable($type)) {
$nullableUnion = true;
continue;
}
$unionTypes[] = $unionType;
}
continue;
}
if ($isNullable($type)) {
$nullableUnion = true;
continue;
}
$unionTypes[] = $type;
}
if (1 === \count($unionTypes)) {
return self::nullable($unionTypes[0]);
}
$unionType = new UnionType(...$unionTypes);
return $nullableUnion ? self::nullable($unionType) : $unionType;
}
public static function intersection(Type ...$types): IntersectionType
{
$intersectionTypes = [];
foreach ($types as $type) {
if (!$type instanceof IntersectionType) {
$intersectionTypes[] = $type;
continue;
}
foreach ($type->getTypes() as $intersectionType) {
$intersectionTypes[] = $intersectionType;
}
}
return new IntersectionType(...$intersectionTypes);
}
public static function nullable(Type $type): Type
{
if ($type->isNullable()) {
return $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) {
$value instanceof \UnitEnum => Type::enum($value::class),
\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)) {
$keyTypes = [];
$valueTypes = [];
foreach ($value as $k => $v) {
$keyTypes[] = self::fromValue($k);
$valueTypes[] = self::fromValue($v);
}
if ($keyTypes) {
$keyTypes = array_values(array_unique($keyTypes));
$keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0];
} else {
$keyType = Type::arrayKey();
}
$valueType = $valueTypes ? CollectionType::mergeCollectionValueTypes($valueTypes) : Type::mixed();
return self::collection($type, $valueType, $keyType, \is_array($value) && [] !== $value && array_is_list($value));
}
if ($value instanceof \ArrayAccess) {
return self::collection($type);
}
return $type;
}
}