* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type as PropertyInfoType; use Symfony\Component\TypeInfo\Type as TypeInfoType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\IntersectionType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\Mapping\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Guesses and loads the appropriate constraints using PropertyInfo. * * @author Kévin Dunglas */ final class PropertyInfoLoader implements LoaderInterface { use AutoMappingTrait; public function __construct( private PropertyListExtractorInterface $listExtractor, private PropertyTypeExtractorInterface $typeExtractor, private PropertyAccessExtractorInterface $accessExtractor, private ?string $classValidatorRegexp = null, ) { } public function loadClassMetadata(ClassMetadata $metadata): bool { $className = $metadata->getClassName(); if (!$properties = $this->listExtractor->getProperties($className)) { return false; } $loaded = false; $enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp); foreach ($properties as $property) { if (false === $this->accessExtractor->isWritable($className, $property)) { continue; } if (!property_exists($className, $property)) { continue; } $types = $this->getPropertyTypes($className, $property); if (null === $types) { continue; } $enabledForProperty = $enabledForClass; $hasTypeConstraint = false; $hasNotNullConstraint = false; $hasNotBlankConstraint = false; $allConstraint = null; foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) { // Enabling or disabling auto-mapping explicitly always takes precedence if (AutoMappingStrategy::DISABLED === $propertyMetadata->getAutoMappingStrategy()) { continue 2; } if (AutoMappingStrategy::ENABLED === $propertyMetadata->getAutoMappingStrategy()) { $enabledForProperty = true; } foreach ($propertyMetadata->getConstraints() as $constraint) { if ($constraint instanceof Type) { $hasTypeConstraint = true; } elseif ($constraint instanceof NotNull) { $hasNotNullConstraint = true; } elseif ($constraint instanceof NotBlank) { $hasNotBlankConstraint = true; } elseif ($constraint instanceof All) { $allConstraint = $constraint; } } } if (!$enabledForProperty) { continue; } $loaded = true; // BC layer for PropertyTypeExtractorInterface::getTypes(). // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). if (\is_array($types)) { $builtinTypes = []; $nullable = false; $scalar = true; foreach ($types as $type) { $builtinTypes[] = $type->getBuiltinType(); if ($scalar && !\in_array($type->getBuiltinType(), ['int', 'float', 'string', 'bool'], true)) { $scalar = false; } if (!$nullable && $type->isNullable()) { $nullable = true; } } if (!$hasTypeConstraint) { if (1 === \count($builtinTypes)) { if ($types[0]->isCollection() && \count($collectionValueType = $types[0]->getCollectionValueTypes()) > 0) { [$collectionValueType] = $collectionValueType; $this->handleAllConstraintLegacy($property, $allConstraint, $collectionValueType, $metadata); } $metadata->addPropertyConstraint($property, $this->getTypeConstraintLegacy($builtinTypes[0], $types[0])); } elseif ($scalar) { $metadata->addPropertyConstraint($property, new Type(['type' => 'scalar'])); } } if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) { $metadata->addPropertyConstraint($property, new NotNull()); } } else { if ($hasTypeConstraint) { continue; } $type = $types; $nullable = false; if ($type instanceof UnionType && $type->isNullable()) { $nullable = true; $type = $type->asNonNullable(); } if ($type instanceof CollectionType) { $this->handleAllConstraint($property, $allConstraint, $type->getCollectionValueType(), $metadata); } if (null !== $typeConstraint = $this->getTypeConstraint($type)) { $metadata->addPropertyConstraint($property, $typeConstraint); } if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) { $metadata->addPropertyConstraint($property, new NotNull()); } } } return $loaded; } /** * BC layer for PropertyTypeExtractorInterface::getTypes(). * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). * * @return TypeInfoType|list|null */ private function getPropertyTypes(string $className, string $property): TypeInfoType|array|null { if (method_exists($this->typeExtractor, 'getType')) { return $this->typeExtractor->getType($className, $property); } return $this->typeExtractor->getTypes($className, $property); } /** * BC layer for PropertyTypeExtractorInterface::getTypes(). * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). */ private function getTypeConstraintLegacy(string $builtinType, PropertyInfoType $type): Type { if (PropertyInfoType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $className = $type->getClassName()) { return new Type(['type' => $className]); } return new Type(['type' => $builtinType]); } private function getTypeConstraint(TypeInfoType $type): ?Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return ($type->isA(TypeIdentifier::INT) || $type->isA(TypeIdentifier::FLOAT) || $type->isA(TypeIdentifier::STRING) || $type->isA(TypeIdentifier::BOOL)) ? new Type(['type' => 'scalar']) : null; } $baseType = $type->getBaseType(); if ($baseType instanceof ObjectType) { return new Type(['type' => $baseType->getClassName()]); } if (TypeIdentifier::MIXED !== $baseType->getTypeIdentifier()) { return new Type(['type' => $baseType->getTypeIdentifier()->value]); } return null; } private function handleAllConstraint(string $property, ?All $allConstraint, TypeInfoType $type, ClassMetadata $metadata): void { $containsTypeConstraint = false; $containsNotNullConstraint = false; if (null !== $allConstraint) { foreach ($allConstraint->constraints as $constraint) { if ($constraint instanceof Type) { $containsTypeConstraint = true; } elseif ($constraint instanceof NotNull) { $containsNotNullConstraint = true; } } } $constraints = []; if (!$containsNotNullConstraint && !$type->isNullable()) { $constraints[] = new NotNull(); } if (!$containsTypeConstraint && null !== $typeConstraint = $this->getTypeConstraint($type)) { $constraints[] = $typeConstraint; } if (!$constraints) { return; } if (null === $allConstraint) { $metadata->addPropertyConstraint($property, new All(['constraints' => $constraints])); } else { $allConstraint->constraints = array_merge($allConstraint->constraints, $constraints); } } /** * BC layer for PropertyTypeExtractorInterface::getTypes(). * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). */ private function handleAllConstraintLegacy(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata): void { $containsTypeConstraint = false; $containsNotNullConstraint = false; if (null !== $allConstraint) { foreach ($allConstraint->constraints as $constraint) { if ($constraint instanceof Type) { $containsTypeConstraint = true; } elseif ($constraint instanceof NotNull) { $containsNotNullConstraint = true; } } } $constraints = []; if (!$containsNotNullConstraint && !$propertyInfoType->isNullable()) { $constraints[] = new NotNull(); } if (!$containsTypeConstraint) { $constraints[] = $this->getTypeConstraintLegacy($propertyInfoType->getBuiltinType(), $propertyInfoType); } if (null === $allConstraint) { $metadata->addPropertyConstraint($property, new All(['constraints' => $constraints])); } else { $allConstraint->constraints = array_merge($allConstraint->constraints, $constraints); } } }