vendor/symfony/doctrine-bridge/PropertyInfo/DoctrineExtractor.php line 215

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bridge\Doctrine\PropertyInfo;
  11. use Doctrine\Common\Collections\Collection;
  12. use Doctrine\DBAL\Types\Types;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use Doctrine\ORM\Mapping\ClassMetadata;
  15. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  16. use Doctrine\ORM\Mapping\Embedded;
  17. use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
  18. use Doctrine\Persistence\Mapping\MappingException;
  19. use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
  20. use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
  21. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  22. use Symfony\Component\PropertyInfo\Type;
  23. /**
  24.  * Extracts data using Doctrine ORM and ODM metadata.
  25.  *
  26.  * @author Kévin Dunglas <dunglas@gmail.com>
  27.  */
  28. class DoctrineExtractor implements PropertyListExtractorInterfacePropertyTypeExtractorInterfacePropertyAccessExtractorInterface
  29. {
  30.     private EntityManagerInterface $entityManager;
  31.     public function __construct(EntityManagerInterface $entityManager)
  32.     {
  33.         $this->entityManager $entityManager;
  34.     }
  35.     public function getProperties(string $class, array $context = []): ?array
  36.     {
  37.         if (null === $metadata $this->getMetadata($class)) {
  38.             return null;
  39.         }
  40.         $properties array_merge($metadata->getFieldNames(), $metadata->getAssociationNames());
  41.         if ($metadata instanceof ClassMetadataInfo && class_exists(Embedded::class) && $metadata->embeddedClasses) {
  42.             $properties array_filter($properties, function ($property) {
  43.                 return !str_contains($property'.');
  44.             });
  45.             $properties array_merge($propertiesarray_keys($metadata->embeddedClasses));
  46.         }
  47.         return $properties;
  48.     }
  49.     public function getTypes(string $classstring $property, array $context = []): ?array
  50.     {
  51.         if (null === $metadata $this->getMetadata($class)) {
  52.             return null;
  53.         }
  54.         if ($metadata->hasAssociation($property)) {
  55.             $class $metadata->getAssociationTargetClass($property);
  56.             if ($metadata->isSingleValuedAssociation($property)) {
  57.                 if ($metadata instanceof ClassMetadataInfo) {
  58.                     $associationMapping $metadata->getAssociationMapping($property);
  59.                     $nullable $this->isAssociationNullable($associationMapping);
  60.                 } else {
  61.                     $nullable false;
  62.                 }
  63.                 return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable$class)];
  64.             }
  65.             $collectionKeyType Type::BUILTIN_TYPE_INT;
  66.             if ($metadata instanceof ClassMetadataInfo) {
  67.                 $associationMapping $metadata->getAssociationMapping($property);
  68.                 if (isset($associationMapping['indexBy'])) {
  69.                     /** @var ClassMetadataInfo $subMetadata */
  70.                     $subMetadata $this->entityManager->getClassMetadata($associationMapping['targetEntity']);
  71.                     // Check if indexBy value is a property
  72.                     $fieldName $associationMapping['indexBy'];
  73.                     if (null === ($typeOfField $subMetadata->getTypeOfField($fieldName))) {
  74.                         $fieldName $subMetadata->getFieldForColumn($associationMapping['indexBy']);
  75.                         // Not a property, maybe a column name?
  76.                         if (null === ($typeOfField $subMetadata->getTypeOfField($fieldName))) {
  77.                             // Maybe the column name is the association join column?
  78.                             $associationMapping $subMetadata->getAssociationMapping($fieldName);
  79.                             /** @var ClassMetadataInfo $subMetadata */
  80.                             $indexProperty $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName);
  81.                             $subMetadata $this->entityManager->getClassMetadata($associationMapping['targetEntity']);
  82.                             // Not a property, maybe a column name?
  83.                             if (null === ($typeOfField $subMetadata->getTypeOfField($indexProperty))) {
  84.                                 $fieldName $subMetadata->getFieldForColumn($indexProperty);
  85.                                 $typeOfField $subMetadata->getTypeOfField($fieldName);
  86.                             }
  87.                         }
  88.                     }
  89.                     if (!$collectionKeyType $this->getPhpType($typeOfField)) {
  90.                         return null;
  91.                     }
  92.                 }
  93.             }
  94.             return [new Type(
  95.                 Type::BUILTIN_TYPE_OBJECT,
  96.                 false,
  97.                 Collection::class,
  98.                 true,
  99.                 new Type($collectionKeyType),
  100.                 new Type(Type::BUILTIN_TYPE_OBJECTfalse$class)
  101.             )];
  102.         }
  103.         if ($metadata instanceof ClassMetadataInfo && class_exists(Embedded::class) && isset($metadata->embeddedClasses[$property])) {
  104.             return [new Type(Type::BUILTIN_TYPE_OBJECTfalse$metadata->embeddedClasses[$property]['class'])];
  105.         }
  106.         if ($metadata->hasField($property)) {
  107.             $typeOfField $metadata->getTypeOfField($property);
  108.             if (!$builtinType $this->getPhpType($typeOfField)) {
  109.                 return null;
  110.             }
  111.             $nullable $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property);
  112.             $enumType null;
  113.             if (null !== $enumClass $metadata->getFieldMapping($property)['enumType'] ?? null) {
  114.                 $enumType = new Type(Type::BUILTIN_TYPE_OBJECT$nullable$enumClass);
  115.             }
  116.             switch ($builtinType) {
  117.                 case Type::BUILTIN_TYPE_OBJECT:
  118.                     switch ($typeOfField) {
  119.                         case Types::DATE_MUTABLE:
  120.                         case Types::DATETIME_MUTABLE:
  121.                         case Types::DATETIMETZ_MUTABLE:
  122.                         case 'vardatetime':
  123.                         case Types::TIME_MUTABLE:
  124.                             return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable'DateTime')];
  125.                         case Types::DATE_IMMUTABLE:
  126.                         case Types::DATETIME_IMMUTABLE:
  127.                         case Types::DATETIMETZ_IMMUTABLE:
  128.                         case Types::TIME_IMMUTABLE:
  129.                             return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable'DateTimeImmutable')];
  130.                         case Types::DATEINTERVAL:
  131.                             return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable'DateInterval')];
  132.                     }
  133.                     break;
  134.                 case Type::BUILTIN_TYPE_ARRAY:
  135.                     switch ($typeOfField) {
  136.                         case Types::ARRAY:
  137.                         case 'json_array':
  138.                             // return null if $enumType is set, because we can't determine if collectionKeyType is string or int
  139.                             if ($enumType) {
  140.                                 return null;
  141.                             }
  142.                             return [new Type(Type::BUILTIN_TYPE_ARRAY$nullablenulltrue)];
  143.                         case Types::SIMPLE_ARRAY:
  144.                             return [new Type(Type::BUILTIN_TYPE_ARRAY$nullablenulltrue, new Type(Type::BUILTIN_TYPE_INT), $enumType ?? new Type(Type::BUILTIN_TYPE_STRING))];
  145.                     }
  146.                     break;
  147.                 case Type::BUILTIN_TYPE_INT:
  148.                 case Type::BUILTIN_TYPE_STRING:
  149.                     if ($enumType) {
  150.                         return [$enumType];
  151.                     }
  152.                     break;
  153.             }
  154.             return [new Type($builtinType$nullable)];
  155.         }
  156.         return null;
  157.     }
  158.     public function isReadable(string $classstring $property, array $context = []): ?bool
  159.     {
  160.         return null;
  161.     }
  162.     public function isWritable(string $classstring $property, array $context = []): ?bool
  163.     {
  164.         if (
  165.             null === ($metadata $this->getMetadata($class))
  166.             || ClassMetadata::GENERATOR_TYPE_NONE === $metadata->generatorType
  167.             || !\in_array($property$metadata->getIdentifierFieldNames(), true)
  168.         ) {
  169.             return null;
  170.         }
  171.         return false;
  172.     }
  173.     private function getMetadata(string $class): ?ClassMetadata
  174.     {
  175.         try {
  176.             return $this->entityManager->getClassMetadata($class);
  177.         } catch (MappingException|OrmMappingException) {
  178.             return null;
  179.         }
  180.     }
  181.     /**
  182.      * Determines whether an association is nullable.
  183.      *
  184.      * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246
  185.      */
  186.     private function isAssociationNullable(array $associationMapping): bool
  187.     {
  188.         if (isset($associationMapping['id']) && $associationMapping['id']) {
  189.             return false;
  190.         }
  191.         if (!isset($associationMapping['joinColumns'])) {
  192.             return true;
  193.         }
  194.         $joinColumns $associationMapping['joinColumns'];
  195.         foreach ($joinColumns as $joinColumn) {
  196.             if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
  197.                 return false;
  198.             }
  199.         }
  200.         return true;
  201.     }
  202.     /**
  203.      * Gets the corresponding built-in PHP type.
  204.      */
  205.     private function getPhpType(string $doctrineType): ?string
  206.     {
  207.         return match ($doctrineType) {
  208.             Types::SMALLINT,
  209.             Types::INTEGER => Type::BUILTIN_TYPE_INT,
  210.             Types::FLOAT => Type::BUILTIN_TYPE_FLOAT,
  211.             Types::BIGINT,
  212.             Types::STRING,
  213.             Types::TEXT,
  214.             Types::GUID,
  215.             Types::DECIMAL => Type::BUILTIN_TYPE_STRING,
  216.             Types::BOOLEAN => Type::BUILTIN_TYPE_BOOL,
  217.             Types::BLOB,
  218.             Types::BINARY => Type::BUILTIN_TYPE_RESOURCE,
  219.             Types::OBJECT,
  220.             Types::DATE_MUTABLE,
  221.             Types::DATETIME_MUTABLE,
  222.             Types::DATETIMETZ_MUTABLE,
  223.             'vardatetime',
  224.             Types::TIME_MUTABLE,
  225.             Types::DATE_IMMUTABLE,
  226.             Types::DATETIME_IMMUTABLE,
  227.             Types::DATETIMETZ_IMMUTABLE,
  228.             Types::TIME_IMMUTABLE,
  229.             Types::DATEINTERVAL => Type::BUILTIN_TYPE_OBJECT,
  230.             Types::ARRAY,
  231.             Types::SIMPLE_ARRAY,
  232.             'json_array' => Type::BUILTIN_TYPE_ARRAY,
  233.             default => null,
  234.         };
  235.     }
  236. }