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

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