vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php line 236

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