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

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\Statement;
  21. use Doctrine\DBAL\FetchMode;
  22. use Doctrine\DBAL\Platforms\AbstractPlatform;
  23. use Doctrine\DBAL\Types\Type;
  24. use Doctrine\ORM\EntityManagerInterface;
  25. use Doctrine\ORM\Events;
  26. use Doctrine\ORM\Mapping\ClassMetadata;
  27. use Doctrine\ORM\Query\ResultSetMapping;
  28. use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
  29. use Doctrine\ORM\UnitOfWork;
  30. use PDO;
  31. use ReflectionClass;
  32. use function array_map;
  33. use function array_merge;
  34. use function count;
  35. use function end;
  36. use function in_array;
  37. use function trigger_error;
  38. use const E_USER_DEPRECATED;
  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
  73.      */
  74.     protected $_metadataCache = [];
  75.     /**
  76.      * The cache used during row-by-row hydration.
  77.      *
  78.      * @var array
  79.      */
  80.     protected $_cache = [];
  81.     /**
  82.      * The statement that provides the data to hydrate.
  83.      *
  84.      * @var Statement
  85.      */
  86.     protected $_stmt;
  87.     /**
  88.      * The query hints.
  89.      *
  90.      * @var array
  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.      * @param array  $hints
  112.      *
  113.      * @return IterableResult
  114.      */
  115.     public function iterate($stmt$resultSetMapping, array $hints = [])
  116.     {
  117.         @trigger_error(
  118.             'Method ' __METHOD__ '() is deprecated and will be removed in Doctrine ORM 3.0. Use toIterable() instead.',
  119.             E_USER_DEPRECATED
  120.         );
  121.         $this->_stmt  $stmt;
  122.         $this->_rsm   $resultSetMapping;
  123.         $this->_hints $hints;
  124.         $evm $this->_em->getEventManager();
  125.         $evm->addEventListener([Events::onClear], $this);
  126.         $this->prepare();
  127.         return new IterableResult($this);
  128.     }
  129.     /**
  130.      * Initiates a row-by-row hydration.
  131.      *
  132.      * @param mixed[] $hints
  133.      *
  134.      * @return iterable<mixed>
  135.      */
  136.     public function toIterable(Statement $stmtResultSetMapping $resultSetMapping, array $hints = []): iterable
  137.     {
  138.         $this->_stmt  $stmt;
  139.         $this->_rsm   $resultSetMapping;
  140.         $this->_hints $hints;
  141.         $evm $this->_em->getEventManager();
  142.         $evm->addEventListener([Events::onClear], $this);
  143.         $this->prepare();
  144.         while (true) {
  145.             $row $this->_stmt->fetch(FetchMode::ASSOCIATIVE);
  146.             if ($row === false || $row === null) {
  147.                 $this->cleanup();
  148.                 break;
  149.             }
  150.             $result = [];
  151.             $this->hydrateRowData($row$result);
  152.             $this->cleanupAfterRowIteration();
  153.             if (count($result) === 1) {
  154.                 yield end($result);
  155.             } else {
  156.                 yield $result;
  157.             }
  158.         }
  159.     }
  160.     /**
  161.      * Hydrates all rows returned by the passed statement instance at once.
  162.      *
  163.      * @param object $stmt
  164.      * @param object $resultSetMapping
  165.      * @param array  $hints
  166.      *
  167.      * @return array
  168.      */
  169.     public function hydrateAll($stmt$resultSetMapping, array $hints = [])
  170.     {
  171.         $this->_stmt  $stmt;
  172.         $this->_rsm   $resultSetMapping;
  173.         $this->_hints $hints;
  174.         $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
  175.         $this->prepare();
  176.         $result $this->hydrateAllData();
  177.         $this->cleanup();
  178.         return $result;
  179.     }
  180.     /**
  181.      * Hydrates a single row returned by the current statement instance during
  182.      * row-by-row hydration with {@link iterate()} or {@link toIterable()}.
  183.      *
  184.      * @return mixed
  185.      */
  186.     public function hydrateRow()
  187.     {
  188.         $row $this->_stmt->fetch(PDO::FETCH_ASSOC);
  189.         if ($row === false || $row === null) {
  190.             $this->cleanup();
  191.             return false;
  192.         }
  193.         $result = [];
  194.         $this->hydrateRowData($row$result);
  195.         return $result;
  196.     }
  197.     /**
  198.      * When executed in a hydrate() loop we have to clear internal state to
  199.      * decrease memory consumption.
  200.      *
  201.      * @param mixed $eventArgs
  202.      *
  203.      * @return void
  204.      */
  205.     public function onClear($eventArgs)
  206.     {
  207.     }
  208.     /**
  209.      * Executes one-time preparation tasks, once each time hydration is started
  210.      * through {@link hydrateAll} or {@link iterate()}.
  211.      *
  212.      * @return void
  213.      */
  214.     protected function prepare()
  215.     {
  216.     }
  217.     /**
  218.      * Executes one-time cleanup tasks at the end of a hydration that was initiated
  219.      * through {@link hydrateAll} or {@link iterate()}.
  220.      *
  221.      * @return void
  222.      */
  223.     protected function cleanup()
  224.     {
  225.         $this->_stmt->closeCursor();
  226.         $this->_stmt          null;
  227.         $this->_rsm           null;
  228.         $this->_cache         = [];
  229.         $this->_metadataCache = [];
  230.         $this
  231.             ->_em
  232.             ->getEventManager()
  233.             ->removeEventListener([Events::onClear], $this);
  234.     }
  235.     protected function cleanupAfterRowIteration(): void
  236.     {
  237.     }
  238.     /**
  239.      * Hydrates a single row from the current statement instance.
  240.      *
  241.      * Template method.
  242.      *
  243.      * @param mixed[] $row    The row data.
  244.      * @param mixed[] $result The result to fill.
  245.      *
  246.      * @return void
  247.      *
  248.      * @throws HydrationException
  249.      */
  250.     protected function hydrateRowData(array $row, array &$result)
  251.     {
  252.         throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
  253.     }
  254.     /**
  255.      * Hydrates all rows from the current statement instance at once.
  256.      *
  257.      * @return mixed[]
  258.      */
  259.     abstract protected function hydrateAllData();
  260.     /**
  261.      * Processes a row of the result set.
  262.      *
  263.      * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
  264.      * Puts the elements of a result row into a new array, grouped by the dql alias
  265.      * they belong to. The column names in the result set are mapped to their
  266.      * field names during this procedure as well as any necessary conversions on
  267.      * the values applied. Scalar values are kept in a specific key 'scalars'.
  268.      *
  269.      * @param mixed[] $data                SQL Result Row.
  270.      * @param array   &$id                 Dql-Alias => ID-Hash.
  271.      * @param array   &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
  272.      *
  273.      * @return array<string, array<string, mixed>> An array with all the fields
  274.      *                                             (name => value) of the data
  275.      *                                             row, grouped by their
  276.      *                                             component alias.
  277.      *
  278.      * @psalm-return array{
  279.      *                   data: array<array-key, array>,
  280.      *                   newObjects?: array<array-key, array{
  281.      *                       class: mixed,
  282.      *                       args?: array
  283.      *                   }>,
  284.      *                   scalars?: array
  285.      *               }
  286.      */
  287.     protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
  288.     {
  289.         $rowData = ['data' => []];
  290.         foreach ($data as $key => $value) {
  291.             if (($cacheKeyInfo $this->hydrateColumnInfo($key)) === null) {
  292.                 continue;
  293.             }
  294.             $fieldName $cacheKeyInfo['fieldName'];
  295.             switch (true) {
  296.                 case isset($cacheKeyInfo['isNewObjectParameter']):
  297.                     $argIndex $cacheKeyInfo['argIndex'];
  298.                     $objIndex $cacheKeyInfo['objIndex'];
  299.                     $type     $cacheKeyInfo['type'];
  300.                     $value    $type->convertToPHPValue($value$this->_platform);
  301.                     $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
  302.                     $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
  303.                     break;
  304.                 case isset($cacheKeyInfo['isScalar']):
  305.                     $type  $cacheKeyInfo['type'];
  306.                     $value $type->convertToPHPValue($value$this->_platform);
  307.                     $rowData['scalars'][$fieldName] = $value;
  308.                     break;
  309.                 //case (isset($cacheKeyInfo['isMetaColumn'])):
  310.                 default:
  311.                     $dqlAlias $cacheKeyInfo['dqlAlias'];
  312.                     $type     $cacheKeyInfo['type'];
  313.                     // If there are field name collisions in the child class, then we need
  314.                     // to only hydrate if we are looking at the correct discriminator value
  315.                     if (
  316.                         isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
  317.                         && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
  318.                     ) {
  319.                         break;
  320.                     }
  321.                     // in an inheritance hierarchy the same field could be defined several times.
  322.                     // We overwrite this value so long we don't have a non-null value, that value we keep.
  323.                     // Per definition it cannot be that a field is defined several times and has several values.
  324.                     if (isset($rowData['data'][$dqlAlias][$fieldName])) {
  325.                         break;
  326.                     }
  327.                     $rowData['data'][$dqlAlias][$fieldName] = $type
  328.                         $type->convertToPHPValue($value$this->_platform)
  329.                         : $value;
  330.                     if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
  331.                         $id[$dqlAlias]                .= '|' $value;
  332.                         $nonemptyComponents[$dqlAlias] = true;
  333.                     }
  334.                     break;
  335.             }
  336.         }
  337.         return $rowData;
  338.     }
  339.     /**
  340.      * Processes a row of the result set.
  341.      *
  342.      * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
  343.      * simply converts column names to field names and properly converts the
  344.      * values according to their types. The resulting row has the same number
  345.      * of elements as before.
  346.      *
  347.      * @param array $data
  348.      *
  349.      * @return array The processed row.
  350.      */
  351.     protected function gatherScalarRowData(&$data)
  352.     {
  353.         $rowData = [];
  354.         foreach ($data as $key => $value) {
  355.             if (($cacheKeyInfo $this->hydrateColumnInfo($key)) === null) {
  356.                 continue;
  357.             }
  358.             $fieldName $cacheKeyInfo['fieldName'];
  359.             // WARNING: BC break! We know this is the desired behavior to type convert values, but this
  360.             // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
  361.             if (! isset($cacheKeyInfo['isScalar'])) {
  362.                 $type  $cacheKeyInfo['type'];
  363.                 $value $type $type->convertToPHPValue($value$this->_platform) : $value;
  364.                 $fieldName $cacheKeyInfo['dqlAlias'] . '_' $fieldName;
  365.             }
  366.             $rowData[$fieldName] = $value;
  367.         }
  368.         return $rowData;
  369.     }
  370.     /**
  371.      * Retrieve column information from ResultSetMapping.
  372.      *
  373.      * @param string $key Column name
  374.      *
  375.      * @return array|null
  376.      */
  377.     protected function hydrateColumnInfo($key)
  378.     {
  379.         if (isset($this->_cache[$key])) {
  380.             return $this->_cache[$key];
  381.         }
  382.         switch (true) {
  383.             // NOTE: Most of the times it's a field mapping, so keep it first!!!
  384.             case isset($this->_rsm->fieldMappings[$key]):
  385.                 $classMetadata $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
  386.                 $fieldName     $this->_rsm->fieldMappings[$key];
  387.                 $fieldMapping  $classMetadata->fieldMappings[$fieldName];
  388.                 $ownerMap      $this->_rsm->columnOwnerMap[$key];
  389.                 $columnInfo    = [
  390.                     'isIdentifier' => in_array($fieldName$classMetadata->identifiertrue),
  391.                     'fieldName'    => $fieldName,
  392.                     'type'         => Type::getType($fieldMapping['type']),
  393.                     'dqlAlias'     => $ownerMap,
  394.                 ];
  395.                 // the current discriminator value must be saved in order to disambiguate fields hydration,
  396.                 // should there be field name collisions
  397.                 if ($classMetadata->parentClasses && isset($this->_rsm->discriminatorColumns[$ownerMap])) {
  398.                     return $this->_cache[$key] = array_merge(
  399.                         $columnInfo,
  400.                         [
  401.                             'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
  402.                             'discriminatorValue'  => $classMetadata->discriminatorValue,
  403.                             'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
  404.                         ]
  405.                     );
  406.                 }
  407.                 return $this->_cache[$key] = $columnInfo;
  408.             case isset($this->_rsm->newObjectMappings[$key]):
  409.                 // WARNING: A NEW object is also a scalar, so it must be declared before!
  410.                 $mapping $this->_rsm->newObjectMappings[$key];
  411.                 return $this->_cache[$key] = [
  412.                     'isScalar'             => true,
  413.                     'isNewObjectParameter' => true,
  414.                     'fieldName'            => $this->_rsm->scalarMappings[$key],
  415.                     'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
  416.                     'argIndex'             => $mapping['argIndex'],
  417.                     'objIndex'             => $mapping['objIndex'],
  418.                     'class'                => new ReflectionClass($mapping['className']),
  419.                 ];
  420.             case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
  421.                 return $this->_cache[$key] = [
  422.                     'fieldName' => $this->_rsm->scalarMappings[$key],
  423.                     'type'      => Type::getType($this->_rsm->typeMappings[$key]),
  424.                     'dqlAlias'  => '',
  425.                 ];
  426.             case isset($this->_rsm->scalarMappings[$key]):
  427.                 return $this->_cache[$key] = [
  428.                     'isScalar'  => true,
  429.                     'fieldName' => $this->_rsm->scalarMappings[$key],
  430.                     'type'      => Type::getType($this->_rsm->typeMappings[$key]),
  431.                 ];
  432.             case isset($this->_rsm->metaMappings[$key]):
  433.                 // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
  434.                 $fieldName $this->_rsm->metaMappings[$key];
  435.                 $dqlAlias  $this->_rsm->columnOwnerMap[$key];
  436.                 $type      = isset($this->_rsm->typeMappings[$key])
  437.                     ? Type::getType($this->_rsm->typeMappings[$key])
  438.                     : null;
  439.                 // Cache metadata fetch
  440.                 $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
  441.                 return $this->_cache[$key] = [
  442.                     'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
  443.                     'isMetaColumn' => true,
  444.                     'fieldName'    => $fieldName,
  445.                     'type'         => $type,
  446.                     'dqlAlias'     => $dqlAlias,
  447.                 ];
  448.         }
  449.         // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
  450.         // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
  451.         return null;
  452.     }
  453.     /**
  454.      * @return string[]
  455.      */
  456.     private function getDiscriminatorValues(ClassMetadata $classMetadata): array
  457.     {
  458.         $values array_map(
  459.             function (string $subClass): string {
  460.                 return (string) $this->getClassMetadata($subClass)->discriminatorValue;
  461.             },
  462.             $classMetadata->subClasses
  463.         );
  464.         $values[] = (string) $classMetadata->discriminatorValue;
  465.         return $values;
  466.     }
  467.     /**
  468.      * Retrieve ClassMetadata associated to entity class name.
  469.      *
  470.      * @param string $className
  471.      *
  472.      * @return ClassMetadata
  473.      */
  474.     protected function getClassMetadata($className)
  475.     {
  476.         if (! isset($this->_metadataCache[$className])) {
  477.             $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
  478.         }
  479.         return $this->_metadataCache[$className];
  480.     }
  481.     /**
  482.      * Register entity as managed in UnitOfWork.
  483.      *
  484.      * @param object  $entity
  485.      * @param mixed[] $data
  486.      *
  487.      * @return void
  488.      *
  489.      * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
  490.      */
  491.     protected function registerManaged(ClassMetadata $class$entity, array $data)
  492.     {
  493.         if ($class->isIdentifierComposite) {
  494.             $id = [];
  495.             foreach ($class->identifier as $fieldName) {
  496.                 $id[$fieldName] = isset($class->associationMappings[$fieldName])
  497.                     ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
  498.                     : $data[$fieldName];
  499.             }
  500.         } else {
  501.             $fieldName $class->identifier[0];
  502.             $id        = [
  503.                 $fieldName => isset($class->associationMappings[$fieldName])
  504.                     ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
  505.                     : $data[$fieldName],
  506.             ];
  507.         }
  508.         $this->_em->getUnitOfWork()->registerManaged($entity$id$data);
  509.     }
  510. }