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

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