* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bridge\Doctrine\PropertyInfo; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as OrmMappingException; use Doctrine\Persistence\Mapping\MappingException; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; /** * Extracts data using Doctrine ORM and ODM metadata. * * @author Kévin Dunglas */ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface { private $entityManager; private $classMetadataFactory; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; } /** * {@inheritdoc} */ public function getProperties(string $class, array $context = []) { if (null === $metadata = $this->getMetadata($class)) { return null; } $properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && $metadata->embeddedClasses) { $properties = array_filter($properties, function ($property) { return false === strpos($property, '.'); }); $properties = array_merge($properties, array_keys($metadata->embeddedClasses)); } return $properties; } /** * {@inheritdoc} */ public function getTypes(string $class, string $property, array $context = []) { if (null === $metadata = $this->getMetadata($class)) { return null; } if ($metadata->hasAssociation($property)) { $class = $metadata->getAssociationTargetClass($property); if ($metadata->isSingleValuedAssociation($property)) { if ($metadata instanceof ClassMetadataInfo) { $associationMapping = $metadata->getAssociationMapping($property); $nullable = $this->isAssociationNullable($associationMapping); } else { $nullable = false; } return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $class)]; } $collectionKeyType = Type::BUILTIN_TYPE_INT; if ($metadata instanceof ClassMetadataInfo) { $associationMapping = $metadata->getAssociationMapping($property); if (isset($associationMapping['indexBy'])) { /** @var ClassMetadataInfo $subMetadata */ $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); // Check if indexBy value is a property $fieldName = $associationMapping['indexBy']; if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { $fieldName = $subMetadata->getFieldForColumn($associationMapping['indexBy']); //Not a property, maybe a column name? if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { //Maybe the column name is the association join column? $associationMapping = $subMetadata->getAssociationMapping($fieldName); /** @var ClassMetadataInfo $subMetadata */ $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName); $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); //Not a property, maybe a column name? if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) { $fieldName = $subMetadata->getFieldForColumn($indexProperty); $typeOfField = $subMetadata->getTypeOfField($fieldName); } } } if (!$collectionKeyType = $this->getPhpType($typeOfField)) { return null; } } } return [new Type( Type::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, new Type($collectionKeyType), new Type(Type::BUILTIN_TYPE_OBJECT, false, $class) )]; } if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && isset($metadata->embeddedClasses[$property])) { return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $metadata->embeddedClasses[$property]['class'])]; } if ($metadata->hasField($property)) { $typeOfField = $metadata->getTypeOfField($property); if (!$builtinType = $this->getPhpType($typeOfField)) { return null; } $nullable = $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property); switch ($builtinType) { case Type::BUILTIN_TYPE_OBJECT: switch ($typeOfField) { case Types::DATE_MUTABLE: case Types::DATETIME_MUTABLE: case Types::DATETIMETZ_MUTABLE: case 'vardatetime': case Types::TIME_MUTABLE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; case Types::DATE_IMMUTABLE: case Types::DATETIME_IMMUTABLE: case Types::DATETIMETZ_IMMUTABLE: case Types::TIME_IMMUTABLE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; case Types::DATEINTERVAL: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; } break; case Type::BUILTIN_TYPE_ARRAY: switch ($typeOfField) { case Types::ARRAY: case Types::JSON_ARRAY: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; case Types::SIMPLE_ARRAY: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]; } } return [new Type($builtinType, $nullable)]; } return null; } /** * {@inheritdoc} */ public function isReadable(string $class, string $property, array $context = []) { return null; } /** * {@inheritdoc} */ public function isWritable(string $class, string $property, array $context = []) { if ( null === ($metadata = $this->getMetadata($class)) || ClassMetadata::GENERATOR_TYPE_NONE === $metadata->generatorType || !\in_array($property, $metadata->getIdentifierFieldNames(), true) ) { return null; } return false; } private function getMetadata(string $class): ?ClassMetadata { try { return $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class); } catch (MappingException | OrmMappingException $exception) { return null; } } /** * Determines whether an association is nullable. * * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246 */ private function isAssociationNullable(array $associationMapping): bool { if (isset($associationMapping['id']) && $associationMapping['id']) { return false; } if (!isset($associationMapping['joinColumns'])) { return true; } $joinColumns = $associationMapping['joinColumns']; foreach ($joinColumns as $joinColumn) { if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) { return false; } } return true; } /** * Gets the corresponding built-in PHP type. */ private function getPhpType(string $doctrineType): ?string { switch ($doctrineType) { case Types::SMALLINT: case Types::INTEGER: return Type::BUILTIN_TYPE_INT; case Types::FLOAT: return Type::BUILTIN_TYPE_FLOAT; case Types::BIGINT: case Types::STRING: case Types::TEXT: case Types::GUID: case Types::DECIMAL: return Type::BUILTIN_TYPE_STRING; case Types::BOOLEAN: return Type::BUILTIN_TYPE_BOOL; case Types::BLOB: case Types::BINARY: return Type::BUILTIN_TYPE_RESOURCE; case Types::OBJECT: case Types::DATE_MUTABLE: case Types::DATETIME_MUTABLE: case Types::DATETIMETZ_MUTABLE: case 'vardatetime': case Types::TIME_MUTABLE: case Types::DATE_IMMUTABLE: case Types::DATETIME_IMMUTABLE: case Types::DATETIMETZ_IMMUTABLE: case Types::TIME_IMMUTABLE: case Types::DATEINTERVAL: return Type::BUILTIN_TYPE_OBJECT; case Types::ARRAY: case Types::SIMPLE_ARRAY: case Types::JSON_ARRAY: return Type::BUILTIN_TYPE_ARRAY; } return null; } }