<?php
namespace Symfony\Component\PropertyInfo\Util;
use phpDocumentor\Reflection\PseudoType;
use phpDocumentor\Reflection\PseudoTypes\ConstExpression;
use phpDocumentor\Reflection\PseudoTypes\Generic;
use phpDocumentor\Reflection\PseudoTypes\List_;
use phpDocumentor\Reflection\PseudoTypes\Scalar;
use phpDocumentor\Reflection\Type as DocType;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Collection;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Mixed_;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Scalar as LegacyScalar;
use phpDocumentor\Reflection\Types\String_;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\TypeIdentifier;
class_exists(List_::class);
final class PhpDocTypeHelper
{
public function getType(DocType $varType): ?Type
{
if ($varType instanceof ConstExpression) {
return null;
}
$nullable = false;
if ($varType instanceof Nullable) {
$nullable = true;
$varType = $varType->getActualType();
}
if (!$varType instanceof Compound) {
if ($varType instanceof Null_) {
$nullable = true;
}
$type = $this->createType($varType);
return $nullable ? Type::nullable($type) : $type;
}
$varTypes = [];
for ($typeIndex = 0; $varType->has($typeIndex); ++$typeIndex) {
$type = $varType->get($typeIndex);
if ($type instanceof ConstExpression) {
return null;
}
if ($type instanceof Null_) {
$nullable = true;
continue;
}
if ($type instanceof Nullable) {
$nullable = true;
$type = $type->getActualType();
}
$varTypes[] = $type;
}
$unionTypes = [];
foreach ($varTypes as $varType) {
if (!$t = $this->createType($varType)) {
continue;
}
if ($t instanceof BuiltinType && TypeIdentifier::MIXED === $t->getTypeIdentifier()) {
return Type::mixed();
}
$unionTypes[] = $t;
}
if (!$unionTypes) {
return null;
}
$type = 1 === \count($unionTypes) ? $unionTypes[0] : Type::union(...$unionTypes);
return $nullable ? Type::nullable($type) : $type;
}
private function createType(DocType $docType): ?Type
{
$docTypeString = (string) $docType;
if ('mixed[]' === $docTypeString) {
$docTypeString = 'array';
}
if ($docType instanceof Generic) {
$fqsen = $docType->getFqsen();
[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);
$collection = is_a($class, \Traversable::class, true) || is_a($class, \ArrayAccess::class, true);
if (!$collection && !class_exists($class, false) && !interface_exists($class, false)) {
return null;
}
$genericTypes = $docType->getTypes();
$type = null !== $class ? Type::object($class) : Type::builtin($phpType);
if ($collection) {
if (null === $valueType = $genericTypes[1] ?? null) {
$keyType = null;
$valueType = $genericTypes[0] ?? null;
} else {
$keyType = $genericTypes[0] ?? null;
}
$value = $valueType ? $this->getType($valueType) : null;
$key = $keyType ? $this->getType($keyType) : null;
return Type::collection($type, $value, $key);
}
$variableTypes = array_map(fn ($t) => $this->getType($t), $genericTypes);
return Type::generic($type, ...array_filter($variableTypes));
}
if ($docType instanceof Collection) {
$fqsen = $docType->getFqsen();
if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
return Type::list($this->getType($docType->getValueType()));
}
[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);
$collection = is_a($class, \Traversable::class, true) || is_a($class, \ArrayAccess::class, true);
if (!$collection && !class_exists($class, false) && !interface_exists($class, false)) {
return null;
}
$type = null !== $class ? Type::object($class) : Type::builtin($phpType);
if ($collection) {
$value = $this->getType($docType->getValueType());
$key = $this->getType($docType->getKeyType());
return Type::collection($type, $value, $key);
}
$variableTypes = [];
if (!$this->hasNoExplicitKeyType($docType) && null !== $keyType = $this->getType($docType->getKeyType())) {
$variableTypes[] = $keyType;
}
if (null !== $valueType = $this->getType($docType->getValueType())) {
$variableTypes[] = $valueType;
}
return Type::generic($type, ...$variableTypes);
}
if (!$docTypeString) {
return null;
}
if ($docType instanceof Array_ && $this->hasNoExplicitKeyType($docType) && str_starts_with($docTypeString, 'array<')) {
return Type::list($this->getType($docType->getValueType()));
}
if (str_ends_with($docTypeString, '[]') && $docType instanceof Array_) {
return Type::list($this->getType($docType->getValueType()));
}
if (str_starts_with($docTypeString, 'list<') && $docType instanceof Array_) {
$collectionValueType = $this->getType($docType->getValueType());
return Type::list($collectionValueType);
}
if (str_starts_with($docTypeString, 'array<') && $docType instanceof Array_) {
$collectionKeyType = $this->getType($docType->getKeyType());
$collectionValueType = $this->getType($docType->getValueType());
return Type::array($collectionValueType, $collectionKeyType);
}
$docTypeString = match ($docTypeString) {
'integer' => 'int',
'boolean' => 'bool',
'double' => 'float',
'callback' => 'callable',
'void' => 'null',
default => $docTypeString,
};
[$phpType, $class] = $this->getPhpTypeAndClass($docTypeString);
if ('array' === $docTypeString) {
return Type::array();
}
if (null === $class) {
return Type::builtin($phpType);
}
if ($docType instanceof LegacyScalar || $docType instanceof Scalar) {
return Type::object('scalar');
}
if ($docType instanceof PseudoType) {
if ($docType->underlyingType() instanceof Integer) {
return Type::int();
}
if ($docType->underlyingType() instanceof String_) {
return Type::string();
}
return null;
}
return Type::object($class);
}
private function hasNoExplicitKeyType(Array_|Collection $type): bool
{
if (method_exists($type, 'getOriginalKeyType')) {
return null === $type->getOriginalKeyType();
}
return $type->getKeyType() instanceof Compound;
}
private function getPhpTypeAndClass(string $docType): array
{
if (\in_array($docType, TypeIdentifier::values(), true)) {
return [$docType, null];
}
if (\in_array($docType, ['parent', 'self', 'static'], true)) {
return ['object', $docType];
}
return ['object', ltrim($docType, '\\')];
}
}