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

Open in your IDE?
  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\BigIntType;
  13. use Doctrine\DBAL\Types\Types;
  14. use Doctrine\ORM\EntityManagerInterface;
  15. use Doctrine\ORM\Mapping\AssociationMapping;
  16. use Doctrine\ORM\Mapping\ClassMetadata;
  17. use Doctrine\ORM\Mapping\EmbeddedClassMapping;
  18. use Doctrine\ORM\Mapping\FieldMapping;
  19. use Doctrine\ORM\Mapping\JoinColumnMapping;
  20. use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
  21. use Doctrine\Persistence\Mapping\MappingException;
  22. use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
  23. use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
  24. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  25. use Symfony\Component\PropertyInfo\Type;
  26. /**
  27.  * Extracts data using Doctrine ORM and ODM metadata.
  28.  *
  29.  * @author Kévin Dunglas <dunglas@gmail.com>
  30.  */
  31. class DoctrineExtractor implements PropertyListExtractorInterfacePropertyTypeExtractorInterfacePropertyAccessExtractorInterface
  32. {
  33.     private $entityManager;
  34.     public function __construct(EntityManagerInterface $entityManager)
  35.     {
  36.         $this->entityManager $entityManager;
  37.     }
  38.     /**
  39.      * {@inheritdoc}
  40.      */
  41.     public function getProperties(string $class, array $context = [])
  42.     {
  43.         if (null === $metadata $this->getMetadata($class)) {
  44.             return null;
  45.         }
  46.         $properties array_merge($metadata->getFieldNames(), $metadata->getAssociationNames());
  47.         if ($metadata instanceof ClassMetadata && $metadata->embeddedClasses) {
  48.             $properties array_filter($properties, function ($property) {
  49.                 return !str_contains($property'.');
  50.             });
  51.             $properties array_merge($propertiesarray_keys($metadata->embeddedClasses));
  52.         }
  53.         return $properties;
  54.     }
  55.     /**
  56.      * {@inheritdoc}
  57.      */
  58.     public function getTypes(string $classstring $property, array $context = [])
  59.     {
  60.         if (null === $metadata $this->getMetadata($class)) {
  61.             return null;
  62.         }
  63.         if ($metadata->hasAssociation($property)) {
  64.             $class $metadata->getAssociationTargetClass($property);
  65.             if ($metadata->isSingleValuedAssociation($property)) {
  66.                 if ($metadata instanceof ClassMetadata) {
  67.                     $associationMapping $metadata->getAssociationMapping($property);
  68.                     $nullable $this->isAssociationNullable($associationMapping);
  69.                 } else {
  70.                     $nullable false;
  71.                 }
  72.                 return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable$class)];
  73.             }
  74.             $collectionKeyType Type::BUILTIN_TYPE_INT;
  75.             if ($metadata instanceof ClassMetadata) {
  76.                 $associationMapping $metadata->getAssociationMapping($property);
  77.                 if (self::getMappingValue($associationMapping'indexBy')) {
  78.                     $subMetadata $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping'targetEntity'));
  79.                     // Check if indexBy value is a property
  80.                     $fieldName self::getMappingValue($associationMapping'indexBy');
  81.                     if (null === ($typeOfField $subMetadata->getTypeOfField($fieldName))) {
  82.                         $fieldName $subMetadata->getFieldForColumn(self::getMappingValue($associationMapping'indexBy'));
  83.                         // Not a property, maybe a column name?
  84.                         if (null === ($typeOfField $subMetadata->getTypeOfField($fieldName))) {
  85.                             // Maybe the column name is the association join column?
  86.                             $associationMapping $subMetadata->getAssociationMapping($fieldName);
  87.                             $indexProperty $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName);
  88.                             $subMetadata $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping'targetEntity'));
  89.                             // Not a property, maybe a column name?
  90.                             if (null === ($typeOfField $subMetadata->getTypeOfField($indexProperty))) {
  91.                                 $fieldName $subMetadata->getFieldForColumn($indexProperty);
  92.                                 $typeOfField $subMetadata->getTypeOfField($fieldName);
  93.                             }
  94.                         }
  95.                     }
  96.                     if (!$collectionKeyType $this->getPhpType($typeOfField)) {
  97.                         return null;
  98.                     }
  99.                 }
  100.             }
  101.             return [new Type(
  102.                 Type::BUILTIN_TYPE_OBJECT,
  103.                 false,
  104.                 Collection::class,
  105.                 true,
  106.                 new Type($collectionKeyType),
  107.                 new Type(Type::BUILTIN_TYPE_OBJECTfalse$class)
  108.             )];
  109.         }
  110.         if ($metadata instanceof ClassMetadata && isset($metadata->embeddedClasses[$property])) {
  111.             return [new Type(Type::BUILTIN_TYPE_OBJECTfalseself::getMappingValue($metadata->embeddedClasses[$property], 'class'))];
  112.         }
  113.         if ($metadata->hasField($property)) {
  114.             $typeOfField $metadata->getTypeOfField($property);
  115.             if (!$builtinType $this->getPhpType($typeOfField)) {
  116.                 return null;
  117.             }
  118.             $nullable $metadata instanceof ClassMetadata && $metadata->isNullable($property);
  119.             // DBAL 4 has a special fallback strategy for BINGINT (int -> string)
  120.             if (Types::BIGINT === $typeOfField && !method_exists(BigIntType::class, 'getName')) {
  121.                 return [
  122.                     new Type(Type::BUILTIN_TYPE_INT$nullable),
  123.                     new Type(Type::BUILTIN_TYPE_STRING$nullable),
  124.                 ];
  125.             }
  126.             $enumType null;
  127.             if (null !== $enumClass self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) {
  128.                 $enumType = new Type(Type::BUILTIN_TYPE_OBJECT$nullable$enumClass);
  129.             }
  130.             switch ($builtinType) {
  131.                 case Type::BUILTIN_TYPE_OBJECT:
  132.                     switch ($typeOfField) {
  133.                         case Types::DATE_MUTABLE:
  134.                         case Types::DATETIME_MUTABLE:
  135.                         case Types::DATETIMETZ_MUTABLE:
  136.                         case 'vardatetime':
  137.                         case Types::TIME_MUTABLE:
  138.                             return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable'DateTime')];
  139.                         case Types::DATE_IMMUTABLE:
  140.                         case Types::DATETIME_IMMUTABLE:
  141.                         case Types::DATETIMETZ_IMMUTABLE:
  142.                         case Types::TIME_IMMUTABLE:
  143.                             return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable'DateTimeImmutable')];
  144.                         case Types::DATEINTERVAL:
  145.                             return [new Type(Type::BUILTIN_TYPE_OBJECT$nullable'DateInterval')];
  146.                     }
  147.                     break;
  148.                 case Type::BUILTIN_TYPE_ARRAY:
  149.                     switch ($typeOfField) {
  150.                         case 'array':      // DBAL < 4
  151.                         case 'json_array'// DBAL < 3
  152.                             // return null if $enumType is set, because we can't determine if collectionKeyType is string or int
  153.                             if ($enumType) {
  154.                                 return null;
  155.                             }
  156.                             return [new Type(Type::BUILTIN_TYPE_ARRAY$nullablenulltrue)];
  157.                         case Types::SIMPLE_ARRAY:
  158.                             return [new Type(Type::BUILTIN_TYPE_ARRAY$nullablenulltrue, new Type(Type::BUILTIN_TYPE_INT), $enumType ?? new Type(Type::BUILTIN_TYPE_STRING))];
  159.                     }
  160.                     break;
  161.                 case Type::BUILTIN_TYPE_INT:
  162.                 case Type::BUILTIN_TYPE_STRING:
  163.                     if ($enumType) {
  164.                         return [$enumType];
  165.                     }
  166.                     break;
  167.             }
  168.             return [new Type($builtinType$nullable)];
  169.         }
  170.         return null;
  171.     }
  172.     /**
  173.      * {@inheritdoc}
  174.      */
  175.     public function isReadable(string $classstring $property, array $context = [])
  176.     {
  177.         return null;
  178.     }
  179.     /**
  180.      * {@inheritdoc}
  181.      */
  182.     public function isWritable(string $classstring $property, array $context = [])
  183.     {
  184.         if (
  185.             null === ($metadata $this->getMetadata($class))
  186.             || ClassMetadata::GENERATOR_TYPE_NONE === $metadata->generatorType
  187.             || !\in_array($property$metadata->getIdentifierFieldNames(), true)
  188.         ) {
  189.             return null;
  190.         }
  191.         return false;
  192.     }
  193.     private function getMetadata(string $class): ?ClassMetadata
  194.     {
  195.         try {
  196.             return $this->entityManager->getClassMetadata($class);
  197.         } catch (MappingException|OrmMappingException $exception) {
  198.             return null;
  199.         }
  200.     }
  201.     /**
  202.      * Determines whether an association is nullable.
  203.      *
  204.      * @param array<string, mixed>|AssociationMapping $associationMapping
  205.      *
  206.      * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246
  207.      */
  208.     private function isAssociationNullable($associationMapping): bool
  209.     {
  210.         if (self::getMappingValue($associationMapping'id')) {
  211.             return false;
  212.         }
  213.         if (!self::getMappingValue($associationMapping'joinColumns')) {
  214.             return true;
  215.         }
  216.         $joinColumns self::getMappingValue($associationMapping'joinColumns');
  217.         foreach ($joinColumns as $joinColumn) {
  218.             if (false === self::getMappingValue($joinColumn'nullable')) {
  219.                 return false;
  220.             }
  221.         }
  222.         return true;
  223.     }
  224.     /**
  225.      * Gets the corresponding built-in PHP type.
  226.      */
  227.     private function getPhpType(string $doctrineType): ?string
  228.     {
  229.         switch ($doctrineType) {
  230.             case Types::SMALLINT:
  231.             case Types::INTEGER:
  232.                 return Type::BUILTIN_TYPE_INT;
  233.             case Types::FLOAT:
  234.                 return Type::BUILTIN_TYPE_FLOAT;
  235.             case Types::BIGINT:
  236.             case Types::STRING:
  237.             case Types::TEXT:
  238.             case Types::GUID:
  239.             case Types::DECIMAL:
  240.                 return Type::BUILTIN_TYPE_STRING;
  241.             case Types::BOOLEAN:
  242.                 return Type::BUILTIN_TYPE_BOOL;
  243.             case Types::BLOB:
  244.             case Types::BINARY:
  245.                 return Type::BUILTIN_TYPE_RESOURCE;
  246.             case 'object'// DBAL < 4
  247.             case Types::DATE_MUTABLE:
  248.             case Types::DATETIME_MUTABLE:
  249.             case Types::DATETIMETZ_MUTABLE:
  250.             case 'vardatetime':
  251.             case Types::TIME_MUTABLE:
  252.             case Types::DATE_IMMUTABLE:
  253.             case Types::DATETIME_IMMUTABLE:
  254.             case Types::DATETIMETZ_IMMUTABLE:
  255.             case Types::TIME_IMMUTABLE:
  256.             case Types::DATEINTERVAL:
  257.                 return Type::BUILTIN_TYPE_OBJECT;
  258.             case 'array'// DBAL < 4
  259.             case Types::SIMPLE_ARRAY:
  260.             case 'json_array'// DBAL < 3
  261.                 return Type::BUILTIN_TYPE_ARRAY;
  262.         }
  263.         return null;
  264.     }
  265.     /**
  266.      * @param array|AssociationMapping|EmbeddedClassMapping|FieldMapping|JoinColumnMapping $mapping
  267.      *
  268.      * @return mixed
  269.      */
  270.     private static function getMappingValue($mappingstring $key)
  271.     {
  272.         if ($mapping instanceof AssociationMapping || $mapping instanceof EmbeddedClassMapping || $mapping instanceof FieldMapping || $mapping instanceof JoinColumnMapping) {
  273.             return $mapping->$key ?? null;
  274.         }
  275.         return $mapping[$key] ?? null;
  276.     }
  277. }