vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php line 168

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Internal\Hydration;
  4. use BackedEnum;
  5. use Doctrine\DBAL\Platforms\AbstractPlatform;
  6. use Doctrine\DBAL\Result;
  7. use Doctrine\DBAL\Types\Type;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use Doctrine\ORM\Events;
  10. use Doctrine\ORM\Mapping\ClassMetadata;
  11. use Doctrine\ORM\Query\ResultSetMapping;
  12. use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
  13. use Doctrine\ORM\UnitOfWork;
  14. use Generator;
  15. use LogicException;
  16. use ReflectionClass;
  17. use function array_map;
  18. use function array_merge;
  19. use function count;
  20. use function end;
  21. use function in_array;
  22. use function is_array;
  23. /**
  24.  * Base class for all hydrators. A hydrator is a class that provides some form
  25.  * of transformation of an SQL result set into another structure.
  26.  *
  27.  * @psalm-consistent-constructor
  28.  */
  29. abstract class AbstractHydrator
  30. {
  31.     /**
  32.      * The ResultSetMapping.
  33.      */
  34.     protected ResultSetMapping|null $rsm null;
  35.     /**
  36.      * The dbms Platform instance.
  37.      */
  38.     protected AbstractPlatform $platform;
  39.     /**
  40.      * The UnitOfWork of the associated EntityManager.
  41.      */
  42.     protected UnitOfWork $uow;
  43.     /**
  44.      * Local ClassMetadata cache to avoid going to the EntityManager all the time.
  45.      *
  46.      * @var array<string, ClassMetadata<object>>
  47.      */
  48.     protected array $metadataCache = [];
  49.     /**
  50.      * The cache used during row-by-row hydration.
  51.      *
  52.      * @var array<string, mixed[]|null>
  53.      */
  54.     protected array $cache = [];
  55.     /**
  56.      * The statement that provides the data to hydrate.
  57.      */
  58.     protected Result|null $stmt null;
  59.     /**
  60.      * The query hints.
  61.      *
  62.      * @var array<string, mixed>
  63.      */
  64.     protected array $hints = [];
  65.     /**
  66.      * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
  67.      */
  68.     public function __construct(protected EntityManagerInterface $em)
  69.     {
  70.         $this->platform $em->getConnection()->getDatabasePlatform();
  71.         $this->uow      $em->getUnitOfWork();
  72.     }
  73.     /**
  74.      * Initiates a row-by-row hydration.
  75.      *
  76.      * @psalm-param array<string, mixed> $hints
  77.      *
  78.      * @return Generator<array-key, mixed>
  79.      *
  80.      * @final
  81.      */
  82.     final public function toIterable(Result $stmtResultSetMapping $resultSetMapping, array $hints = []): Generator
  83.     {
  84.         $this->stmt  $stmt;
  85.         $this->rsm   $resultSetMapping;
  86.         $this->hints $hints;
  87.         $evm $this->em->getEventManager();
  88.         $evm->addEventListener([Events::onClear], $this);
  89.         $this->prepare();
  90.         try {
  91.             while (true) {
  92.                 $row $this->statement()->fetchAssociative();
  93.                 if ($row === false) {
  94.                     break;
  95.                 }
  96.                 $result = [];
  97.                 $this->hydrateRowData($row$result);
  98.                 $this->cleanupAfterRowIteration();
  99.                 if (count($result) === 1) {
  100.                     if (count($resultSetMapping->indexByMap) === 0) {
  101.                         yield end($result);
  102.                     } else {
  103.                         yield from $result;
  104.                     }
  105.                 } else {
  106.                     yield $result;
  107.                 }
  108.             }
  109.         } finally {
  110.             $this->cleanup();
  111.         }
  112.     }
  113.     final protected function statement(): Result
  114.     {
  115.         if ($this->stmt === null) {
  116.             throw new LogicException('Uninitialized _stmt property');
  117.         }
  118.         return $this->stmt;
  119.     }
  120.     final protected function resultSetMapping(): ResultSetMapping
  121.     {
  122.         if ($this->rsm === null) {
  123.             throw new LogicException('Uninitialized _rsm property');
  124.         }
  125.         return $this->rsm;
  126.     }
  127.     /**
  128.      * Hydrates all rows returned by the passed statement instance at once.
  129.      *
  130.      * @psalm-param array<string, string> $hints
  131.      */
  132.     public function hydrateAll(Result $stmtResultSetMapping $resultSetMapping, array $hints = []): mixed
  133.     {
  134.         $this->stmt  $stmt;
  135.         $this->rsm   $resultSetMapping;
  136.         $this->hints $hints;
  137.         $this->em->getEventManager()->addEventListener([Events::onClear], $this);
  138.         $this->prepare();
  139.         try {
  140.             $result $this->hydrateAllData();
  141.         } finally {
  142.             $this->cleanup();
  143.         }
  144.         return $result;
  145.     }
  146.     /**
  147.      * When executed in a hydrate() loop we have to clear internal state to
  148.      * decrease memory consumption.
  149.      */
  150.     public function onClear(mixed $eventArgs): void
  151.     {
  152.     }
  153.     /**
  154.      * Executes one-time preparation tasks, once each time hydration is started
  155.      * through {@link hydrateAll} or {@link toIterable()}.
  156.      */
  157.     protected function prepare(): void
  158.     {
  159.     }
  160.     /**
  161.      * Executes one-time cleanup tasks at the end of a hydration that was initiated
  162.      * through {@link hydrateAll} or {@link toIterable()}.
  163.      */
  164.     protected function cleanup(): void
  165.     {
  166.         $this->statement()->free();
  167.         $this->stmt          null;
  168.         $this->rsm           null;
  169.         $this->cache         = [];
  170.         $this->metadataCache = [];
  171.         $this
  172.             ->em
  173.             ->getEventManager()
  174.             ->removeEventListener([Events::onClear], $this);
  175.     }
  176.     protected function cleanupAfterRowIteration(): void
  177.     {
  178.     }
  179.     /**
  180.      * Hydrates a single row from the current statement instance.
  181.      *
  182.      * Template method.
  183.      *
  184.      * @param mixed[] $row    The row data.
  185.      * @param mixed[] $result The result to fill.
  186.      *
  187.      * @throws HydrationException
  188.      */
  189.     protected function hydrateRowData(array $row, array &$result): void
  190.     {
  191.         throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
  192.     }
  193.     /**
  194.      * Hydrates all rows from the current statement instance at once.
  195.      */
  196.     abstract protected function hydrateAllData(): mixed;
  197.     /**
  198.      * Processes a row of the result set.
  199.      *
  200.      * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
  201.      * Puts the elements of a result row into a new array, grouped by the dql alias
  202.      * they belong to. The column names in the result set are mapped to their
  203.      * field names during this procedure as well as any necessary conversions on
  204.      * the values applied. Scalar values are kept in a specific key 'scalars'.
  205.      *
  206.      * @param mixed[] $data SQL Result Row.
  207.      * @psalm-param array<string, string> $id                 Dql-Alias => ID-Hash.
  208.      * @psalm-param array<string, bool>   $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
  209.      *
  210.      * @return array<string, array<string, mixed>> An array with all the fields
  211.      *                                             (name => value) of the data
  212.      *                                             row, grouped by their
  213.      *                                             component alias.
  214.      * @psalm-return array{
  215.      *                   data: array<array-key, array>,
  216.      *                   newObjects?: array<array-key, array{
  217.      *                       class: mixed,
  218.      *                       args?: array
  219.      *                   }>,
  220.      *                   scalars?: array
  221.      *               }
  222.      */
  223.     protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
  224.     {
  225.         $rowData = ['data' => []];
  226.         foreach ($data as $key => $value) {
  227.             $cacheKeyInfo $this->hydrateColumnInfo($key);
  228.             if ($cacheKeyInfo === null) {
  229.                 continue;
  230.             }
  231.             $fieldName $cacheKeyInfo['fieldName'];
  232.             switch (true) {
  233.                 case isset($cacheKeyInfo['isNewObjectParameter']):
  234.                     $argIndex $cacheKeyInfo['argIndex'];
  235.                     $objIndex $cacheKeyInfo['objIndex'];
  236.                     $type     $cacheKeyInfo['type'];
  237.                     $value    $type->convertToPHPValue($value$this->platform);
  238.                     if ($value !== null && isset($cacheKeyInfo['enumType'])) {
  239.                         $value $this->buildEnum($value$cacheKeyInfo['enumType']);
  240.                     }
  241.                     $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
  242.                     $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
  243.                     break;
  244.                 case isset($cacheKeyInfo['isScalar']):
  245.                     $type  $cacheKeyInfo['type'];
  246.                     $value $type->convertToPHPValue($value$this->platform);
  247.                     if ($value !== null && isset($cacheKeyInfo['enumType'])) {
  248.                         $value $this->buildEnum($value$cacheKeyInfo['enumType']);
  249.                     }
  250.                     $rowData['scalars'][$fieldName] = $value;
  251.                     break;
  252.                 //case (isset($cacheKeyInfo['isMetaColumn'])):
  253.                 default:
  254.                     $dqlAlias $cacheKeyInfo['dqlAlias'];
  255.                     $type     $cacheKeyInfo['type'];
  256.                     // If there are field name collisions in the child class, then we need
  257.                     // to only hydrate if we are looking at the correct discriminator value
  258.                     if (
  259.                         isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
  260.                         && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
  261.                     ) {
  262.                         break;
  263.                     }
  264.                     // in an inheritance hierarchy the same field could be defined several times.
  265.                     // We overwrite this value so long we don't have a non-null value, that value we keep.
  266.                     // Per definition it cannot be that a field is defined several times and has several values.
  267.                     if (isset($rowData['data'][$dqlAlias][$fieldName])) {
  268.                         break;
  269.                     }
  270.                     $rowData['data'][$dqlAlias][$fieldName] = $type
  271.                         $type->convertToPHPValue($value$this->platform)
  272.                         : $value;
  273.                     if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) {
  274.                         $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']);
  275.                     }
  276.                     if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
  277.                         $id[$dqlAlias]                .= '|' $value;
  278.                         $nonemptyComponents[$dqlAlias] = true;
  279.                     }
  280.                     break;
  281.             }
  282.         }
  283.         return $rowData;
  284.     }
  285.     /**
  286.      * Processes a row of the result set.
  287.      *
  288.      * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
  289.      * simply converts column names to field names and properly converts the
  290.      * values according to their types. The resulting row has the same number
  291.      * of elements as before.
  292.      *
  293.      * @param mixed[] $data
  294.      * @psalm-param array<string, mixed> $data
  295.      *
  296.      * @return mixed[] The processed row.
  297.      * @psalm-return array<string, mixed>
  298.      */
  299.     protected function gatherScalarRowData(array &$data): array
  300.     {
  301.         $rowData = [];
  302.         foreach ($data as $key => $value) {
  303.             $cacheKeyInfo $this->hydrateColumnInfo($key);
  304.             if ($cacheKeyInfo === null) {
  305.                 continue;
  306.             }
  307.             $fieldName $cacheKeyInfo['fieldName'];
  308.             // WARNING: BC break! We know this is the desired behavior to type convert values, but this
  309.             // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
  310.             if (! isset($cacheKeyInfo['isScalar'])) {
  311.                 $type  $cacheKeyInfo['type'];
  312.                 $value $type $type->convertToPHPValue($value$this->platform) : $value;
  313.                 $fieldName $cacheKeyInfo['dqlAlias'] . '_' $fieldName;
  314.             }
  315.             $rowData[$fieldName] = $value;
  316.         }
  317.         return $rowData;
  318.     }
  319.     /**
  320.      * Retrieve column information from ResultSetMapping.
  321.      *
  322.      * @param string $key Column name
  323.      *
  324.      * @return mixed[]|null
  325.      * @psalm-return array<string, mixed>|null
  326.      */
  327.     protected function hydrateColumnInfo(string $key): array|null
  328.     {
  329.         if (isset($this->cache[$key])) {
  330.             return $this->cache[$key];
  331.         }
  332.         switch (true) {
  333.             // NOTE: Most of the times it's a field mapping, so keep it first!!!
  334.             case isset($this->rsm->fieldMappings[$key]):
  335.                 $classMetadata $this->getClassMetadata($this->rsm->declaringClasses[$key]);
  336.                 $fieldName     $this->rsm->fieldMappings[$key];
  337.                 $fieldMapping  $classMetadata->fieldMappings[$fieldName];
  338.                 $ownerMap      $this->rsm->columnOwnerMap[$key];
  339.                 $columnInfo    = [
  340.                     'isIdentifier' => in_array($fieldName$classMetadata->identifiertrue),
  341.                     'fieldName'    => $fieldName,
  342.                     'type'         => Type::getType($fieldMapping->type),
  343.                     'dqlAlias'     => $ownerMap,
  344.                     'enumType'     => $this->rsm->enumMappings[$key] ?? null,
  345.                 ];
  346.                 // the current discriminator value must be saved in order to disambiguate fields hydration,
  347.                 // should there be field name collisions
  348.                 if ($classMetadata->parentClasses && isset($this->rsm->discriminatorColumns[$ownerMap])) {
  349.                     return $this->cache[$key] = array_merge(
  350.                         $columnInfo,
  351.                         [
  352.                             'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
  353.                             'discriminatorValue'  => $classMetadata->discriminatorValue,
  354.                             'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
  355.                         ],
  356.                     );
  357.                 }
  358.                 return $this->cache[$key] = $columnInfo;
  359.             case isset($this->rsm->newObjectMappings[$key]):
  360.                 // WARNING: A NEW object is also a scalar, so it must be declared before!
  361.                 $mapping $this->rsm->newObjectMappings[$key];
  362.                 return $this->cache[$key] = [
  363.                     'isScalar'             => true,
  364.                     'isNewObjectParameter' => true,
  365.                     'fieldName'            => $this->rsm->scalarMappings[$key],
  366.                     'type'                 => Type::getType($this->rsm->typeMappings[$key]),
  367.                     'argIndex'             => $mapping['argIndex'],
  368.                     'objIndex'             => $mapping['objIndex'],
  369.                     'class'                => new ReflectionClass($mapping['className']),
  370.                     'enumType'             => $this->rsm->enumMappings[$key] ?? null,
  371.                 ];
  372.             case isset($this->rsm->scalarMappings[$key], $this->hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
  373.                 return $this->cache[$key] = [
  374.                     'fieldName' => $this->rsm->scalarMappings[$key],
  375.                     'type'      => Type::getType($this->rsm->typeMappings[$key]),
  376.                     'dqlAlias'  => '',
  377.                     'enumType'  => $this->rsm->enumMappings[$key] ?? null,
  378.                 ];
  379.             case isset($this->rsm->scalarMappings[$key]):
  380.                 return $this->cache[$key] = [
  381.                     'isScalar'  => true,
  382.                     'fieldName' => $this->rsm->scalarMappings[$key],
  383.                     'type'      => Type::getType($this->rsm->typeMappings[$key]),
  384.                     'enumType'  => $this->rsm->enumMappings[$key] ?? null,
  385.                 ];
  386.             case isset($this->rsm->metaMappings[$key]):
  387.                 // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
  388.                 $fieldName $this->rsm->metaMappings[$key];
  389.                 $dqlAlias  $this->rsm->columnOwnerMap[$key];
  390.                 $type      = isset($this->rsm->typeMappings[$key])
  391.                     ? Type::getType($this->rsm->typeMappings[$key])
  392.                     : null;
  393.                 // Cache metadata fetch
  394.                 $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
  395.                 return $this->cache[$key] = [
  396.                     'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
  397.                     'isMetaColumn' => true,
  398.                     'fieldName'    => $fieldName,
  399.                     'type'         => $type,
  400.                     'dqlAlias'     => $dqlAlias,
  401.                     'enumType'     => $this->rsm->enumMappings[$key] ?? null,
  402.                 ];
  403.         }
  404.         // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
  405.         // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
  406.         return null;
  407.     }
  408.     /**
  409.      * @return string[]
  410.      * @psalm-return non-empty-list<string>
  411.      */
  412.     private function getDiscriminatorValues(ClassMetadata $classMetadata): array
  413.     {
  414.         $values array_map(
  415.             fn (string $subClass): string => (string) $this->getClassMetadata($subClass)->discriminatorValue,
  416.             $classMetadata->subClasses,
  417.         );
  418.         $values[] = (string) $classMetadata->discriminatorValue;
  419.         return $values;
  420.     }
  421.     /**
  422.      * Retrieve ClassMetadata associated to entity class name.
  423.      */
  424.     protected function getClassMetadata(string $className): ClassMetadata
  425.     {
  426.         if (! isset($this->metadataCache[$className])) {
  427.             $this->metadataCache[$className] = $this->em->getClassMetadata($className);
  428.         }
  429.         return $this->metadataCache[$className];
  430.     }
  431.     /**
  432.      * Register entity as managed in UnitOfWork.
  433.      *
  434.      * @param mixed[] $data
  435.      *
  436.      * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
  437.      */
  438.     protected function registerManaged(ClassMetadata $classobject $entity, array $data): void
  439.     {
  440.         if ($class->isIdentifierComposite) {
  441.             $id = [];
  442.             foreach ($class->identifier as $fieldName) {
  443.                 $id[$fieldName] = isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide()
  444.                     ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
  445.                     : $data[$fieldName];
  446.             }
  447.         } else {
  448.             $fieldName $class->identifier[0];
  449.             $id        = [
  450.                 $fieldName => isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide()
  451.                     ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
  452.                     : $data[$fieldName],
  453.             ];
  454.         }
  455.         $this->em->getUnitOfWork()->registerManaged($entity$id$data);
  456.     }
  457.     /**
  458.      * @param class-string<BackedEnum> $enumType
  459.      *
  460.      * @return BackedEnum|array<BackedEnum>
  461.      */
  462.     final protected function buildEnum(mixed $valuestring $enumType): BackedEnum|array
  463.     {
  464.         if (is_array($value)) {
  465.             return array_map(
  466.                 static fn ($value) => $enumType::from($value),
  467.                 $value,
  468.             );
  469.         }
  470.         return $enumType::from($value);
  471.     }
  472. }