<?php 
 
declare(strict_types=1); 
 
namespace Doctrine\ORM\Internal\Hydration; 
 
use BackedEnum; 
use Doctrine\DBAL\Driver\ResultStatement; 
use Doctrine\DBAL\ForwardCompatibility\Result as ForwardCompatibilityResult; 
use Doctrine\DBAL\Platforms\AbstractPlatform; 
use Doctrine\DBAL\Result; 
use Doctrine\DBAL\Types\Type; 
use Doctrine\Deprecations\Deprecation; 
use Doctrine\ORM\EntityManagerInterface; 
use Doctrine\ORM\Events; 
use Doctrine\ORM\Mapping\ClassMetadata; 
use Doctrine\ORM\Query\ResultSetMapping; 
use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker; 
use Doctrine\ORM\UnitOfWork; 
use Generator; 
use LogicException; 
use ReflectionClass; 
use TypeError; 
 
use function array_map; 
use function array_merge; 
use function count; 
use function end; 
use function get_debug_type; 
use function in_array; 
use function is_array; 
use function sprintf; 
 
/** 
 * Base class for all hydrators. A hydrator is a class that provides some form 
 * of transformation of an SQL result set into another structure. 
 */ 
abstract class AbstractHydrator 
{ 
    /** 
     * The ResultSetMapping. 
     * 
     * @var ResultSetMapping|null 
     */ 
    protected $_rsm; 
 
    /** 
     * The EntityManager instance. 
     * 
     * @var EntityManagerInterface 
     */ 
    protected $_em; 
 
    /** 
     * The dbms Platform instance. 
     * 
     * @var AbstractPlatform 
     */ 
    protected $_platform; 
 
    /** 
     * The UnitOfWork of the associated EntityManager. 
     * 
     * @var UnitOfWork 
     */ 
    protected $_uow; 
 
    /** 
     * Local ClassMetadata cache to avoid going to the EntityManager all the time. 
     * 
     * @var array<string, ClassMetadata<object>> 
     */ 
    protected $_metadataCache = []; 
 
    /** 
     * The cache used during row-by-row hydration. 
     * 
     * @var array<string, mixed[]|null> 
     */ 
    protected $_cache = []; 
 
    /** 
     * The statement that provides the data to hydrate. 
     * 
     * @var Result|null 
     */ 
    protected $_stmt; 
 
    /** 
     * The query hints. 
     * 
     * @var array<string, mixed> 
     */ 
    protected $_hints = []; 
 
    /** 
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>. 
     * 
     * @param EntityManagerInterface $em The EntityManager to use. 
     */ 
    public function __construct(EntityManagerInterface $em) 
    { 
        $this->_em       = $em; 
        $this->_platform = $em->getConnection()->getDatabasePlatform(); 
        $this->_uow      = $em->getUnitOfWork(); 
    } 
 
    /** 
     * Initiates a row-by-row hydration. 
     * 
     * @deprecated 
     * 
     * @param Result|ResultStatement $stmt 
     * @param ResultSetMapping       $resultSetMapping 
     * @psalm-param array<string, mixed> $hints 
     * 
     * @return IterableResult 
     */ 
    public function iterate($stmt, $resultSetMapping, array $hints = []) 
    { 
        Deprecation::trigger( 
            'doctrine/orm', 
            'https://github.com/doctrine/orm/issues/8463', 
            'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use toIterable() instead.', 
            __METHOD__ 
        ); 
 
        $this->_stmt  = $stmt instanceof ResultStatement ? ForwardCompatibilityResult::ensure($stmt) : $stmt; 
        $this->_rsm   = $resultSetMapping; 
        $this->_hints = $hints; 
 
        $evm = $this->_em->getEventManager(); 
 
        $evm->addEventListener([Events::onClear], $this); 
 
        $this->prepare(); 
 
        return new IterableResult($this); 
    } 
 
    /** 
     * Initiates a row-by-row hydration. 
     * 
     * @param Result|ResultStatement $stmt 
     * @psalm-param array<string, mixed> $hints 
     * 
     * @return Generator<array-key, mixed> 
     * 
     * @final 
     */ 
    public function toIterable($stmt, ResultSetMapping $resultSetMapping, array $hints = []): iterable 
    { 
        if (! $stmt instanceof Result) { 
            if (! $stmt instanceof ResultStatement) { 
                throw new TypeError(sprintf( 
                    '%s: Expected parameter $stmt to be an instance of %s or %s, got %s', 
                    __METHOD__, 
                    Result::class, 
                    ResultStatement::class, 
                    get_debug_type($stmt) 
                )); 
            } 
 
            Deprecation::trigger( 
                'doctrine/orm', 
                'https://github.com/doctrine/orm/pull/8796', 
                '%s: Passing a result as $stmt that does not implement %s is deprecated and will cause a TypeError on 3.0', 
                __METHOD__, 
                Result::class 
            ); 
 
            $stmt = ForwardCompatibilityResult::ensure($stmt); 
        } 
 
        $this->_stmt  = $stmt; 
        $this->_rsm   = $resultSetMapping; 
        $this->_hints = $hints; 
 
        $evm = $this->_em->getEventManager(); 
 
        $evm->addEventListener([Events::onClear], $this); 
 
        $this->prepare(); 
 
        while (true) { 
            $row = $this->statement()->fetchAssociative(); 
 
            if ($row === false) { 
                $this->cleanup(); 
 
                break; 
            } 
 
            $result = []; 
 
            $this->hydrateRowData($row, $result); 
 
            $this->cleanupAfterRowIteration(); 
            if (count($result) === 1) { 
                if (count($resultSetMapping->indexByMap) === 0) { 
                    yield end($result); 
                } else { 
                    yield from $result; 
                } 
            } else { 
                yield $result; 
            } 
        } 
    } 
 
    final protected function statement(): Result 
    { 
        if ($this->_stmt === null) { 
            throw new LogicException('Uninitialized _stmt property'); 
        } 
 
        return $this->_stmt; 
    } 
 
    final protected function resultSetMapping(): ResultSetMapping 
    { 
        if ($this->_rsm === null) { 
            throw new LogicException('Uninitialized _rsm property'); 
        } 
 
        return $this->_rsm; 
    } 
 
    /** 
     * Hydrates all rows returned by the passed statement instance at once. 
     * 
     * @param Result|ResultStatement $stmt 
     * @param ResultSetMapping       $resultSetMapping 
     * @psalm-param array<string, string> $hints 
     * 
     * @return mixed[] 
     */ 
    public function hydrateAll($stmt, $resultSetMapping, array $hints = []) 
    { 
        if (! $stmt instanceof Result) { 
            if (! $stmt instanceof ResultStatement) { 
                throw new TypeError(sprintf( 
                    '%s: Expected parameter $stmt to be an instance of %s or %s, got %s', 
                    __METHOD__, 
                    Result::class, 
                    ResultStatement::class, 
                    get_debug_type($stmt) 
                )); 
            } 
 
            Deprecation::trigger( 
                'doctrine/orm', 
                'https://github.com/doctrine/orm/pull/8796', 
                '%s: Passing a result as $stmt that does not implement %s is deprecated and will cause a TypeError on 3.0', 
                __METHOD__, 
                Result::class 
            ); 
 
            $stmt = ForwardCompatibilityResult::ensure($stmt); 
        } 
 
        $this->_stmt  = $stmt; 
        $this->_rsm   = $resultSetMapping; 
        $this->_hints = $hints; 
 
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this); 
        $this->prepare(); 
 
        try { 
            $result = $this->hydrateAllData(); 
        } finally { 
            $this->cleanup(); 
        } 
 
        return $result; 
    } 
 
    /** 
     * Hydrates a single row returned by the current statement instance during 
     * row-by-row hydration with {@link iterate()} or {@link toIterable()}. 
     * 
     * @deprecated 
     * 
     * @return mixed[]|false 
     */ 
    public function hydrateRow() 
    { 
        Deprecation::triggerIfCalledFromOutside( 
            'doctrine/orm', 
            'https://github.com/doctrine/orm/pull/9072', 
            '%s is deprecated.', 
            __METHOD__ 
        ); 
 
        $row = $this->statement()->fetchAssociative(); 
 
        if ($row === false) { 
            $this->cleanup(); 
 
            return false; 
        } 
 
        $result = []; 
 
        $this->hydrateRowData($row, $result); 
 
        return $result; 
    } 
 
    /** 
     * When executed in a hydrate() loop we have to clear internal state to 
     * decrease memory consumption. 
     * 
     * @param mixed $eventArgs 
     * 
     * @return void 
     */ 
    public function onClear($eventArgs) 
    { 
    } 
 
    /** 
     * Executes one-time preparation tasks, once each time hydration is started 
     * through {@link hydrateAll} or {@link iterate()}. 
     * 
     * @return void 
     */ 
    protected function prepare() 
    { 
    } 
 
    /** 
     * Executes one-time cleanup tasks at the end of a hydration that was initiated 
     * through {@link hydrateAll} or {@link iterate()}. 
     * 
     * @return void 
     */ 
    protected function cleanup() 
    { 
        $this->statement()->free(); 
 
        $this->_stmt          = null; 
        $this->_rsm           = null; 
        $this->_cache         = []; 
        $this->_metadataCache = []; 
 
        $this 
            ->_em 
            ->getEventManager() 
            ->removeEventListener([Events::onClear], $this); 
    } 
 
    protected function cleanupAfterRowIteration(): void 
    { 
    } 
 
    /** 
     * Hydrates a single row from the current statement instance. 
     * 
     * Template method. 
     * 
     * @param mixed[] $row    The row data. 
     * @param mixed[] $result The result to fill. 
     * 
     * @return void 
     * 
     * @throws HydrationException 
     */ 
    protected function hydrateRowData(array $row, array &$result) 
    { 
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.'); 
    } 
 
    /** 
     * Hydrates all rows from the current statement instance at once. 
     * 
     * @return mixed[] 
     */ 
    abstract protected function hydrateAllData(); 
 
    /** 
     * Processes a row of the result set. 
     * 
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY). 
     * Puts the elements of a result row into a new array, grouped by the dql alias 
     * they belong to. The column names in the result set are mapped to their 
     * field names during this procedure as well as any necessary conversions on 
     * the values applied. Scalar values are kept in a specific key 'scalars'. 
     * 
     * @param mixed[] $data SQL Result Row. 
     * @psalm-param array<string, string> $id                 Dql-Alias => ID-Hash. 
     * @psalm-param array<string, bool>   $nonemptyComponents Does this DQL-Alias has at least one non NULL value? 
     * 
     * @return array<string, array<string, mixed>> An array with all the fields 
     *                                             (name => value) of the data 
     *                                             row, grouped by their 
     *                                             component alias. 
     * @psalm-return array{ 
     *                   data: array<array-key, array>, 
     *                   newObjects?: array<array-key, array{ 
     *                       class: mixed, 
     *                       args?: array 
     *                   }>, 
     *                   scalars?: array 
     *               } 
     */ 
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents) 
    { 
        $rowData = ['data' => []]; 
 
        foreach ($data as $key => $value) { 
            $cacheKeyInfo = $this->hydrateColumnInfo($key); 
            if ($cacheKeyInfo === null) { 
                continue; 
            } 
 
            $fieldName = $cacheKeyInfo['fieldName']; 
 
            switch (true) { 
                case isset($cacheKeyInfo['isNewObjectParameter']): 
                    $argIndex = $cacheKeyInfo['argIndex']; 
                    $objIndex = $cacheKeyInfo['objIndex']; 
                    $type     = $cacheKeyInfo['type']; 
                    $value    = $type->convertToPHPValue($value, $this->_platform); 
 
                    if ($value !== null && isset($cacheKeyInfo['enumType'])) { 
                        $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); 
                    } 
 
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class']; 
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value; 
                    break; 
 
                case isset($cacheKeyInfo['isScalar']): 
                    $type  = $cacheKeyInfo['type']; 
                    $value = $type->convertToPHPValue($value, $this->_platform); 
 
                    if ($value !== null && isset($cacheKeyInfo['enumType'])) { 
                        $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); 
                    } 
 
                    $rowData['scalars'][$fieldName] = $value; 
 
                    break; 
 
                //case (isset($cacheKeyInfo['isMetaColumn'])): 
                default: 
                    $dqlAlias = $cacheKeyInfo['dqlAlias']; 
                    $type     = $cacheKeyInfo['type']; 
 
                    // If there are field name collisions in the child class, then we need 
                    // to only hydrate if we are looking at the correct discriminator value 
                    if ( 
                        isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) 
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true) 
                    ) { 
                        break; 
                    } 
 
                    // in an inheritance hierarchy the same field could be defined several times. 
                    // We overwrite this value so long we don't have a non-null value, that value we keep. 
                    // Per definition it cannot be that a field is defined several times and has several values. 
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) { 
                        break; 
                    } 
 
                    $rowData['data'][$dqlAlias][$fieldName] = $type 
                        ? $type->convertToPHPValue($value, $this->_platform) 
                        : $value; 
 
                    if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) { 
                        $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']); 
                    } 
 
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) { 
                        $id[$dqlAlias]                .= '|' . $value; 
                        $nonemptyComponents[$dqlAlias] = true; 
                    } 
 
                    break; 
            } 
        } 
 
        return $rowData; 
    } 
 
    /** 
     * Processes a row of the result set. 
     * 
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that 
     * simply converts column names to field names and properly converts the 
     * values according to their types. The resulting row has the same number 
     * of elements as before. 
     * 
     * @param mixed[] $data 
     * @psalm-param array<string, mixed> $data 
     * 
     * @return mixed[] The processed row. 
     * @psalm-return array<string, mixed> 
     */ 
    protected function gatherScalarRowData(&$data) 
    { 
        $rowData = []; 
 
        foreach ($data as $key => $value) { 
            $cacheKeyInfo = $this->hydrateColumnInfo($key); 
            if ($cacheKeyInfo === null) { 
                continue; 
            } 
 
            $fieldName = $cacheKeyInfo['fieldName']; 
 
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this 
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility. 
            if (! isset($cacheKeyInfo['isScalar'])) { 
                $type  = $cacheKeyInfo['type']; 
                $value = $type ? $type->convertToPHPValue($value, $this->_platform) : $value; 
 
                $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName; 
            } 
 
            $rowData[$fieldName] = $value; 
        } 
 
        return $rowData; 
    } 
 
    /** 
     * Retrieve column information from ResultSetMapping. 
     * 
     * @param string $key Column name 
     * 
     * @return mixed[]|null 
     * @psalm-return array<string, mixed>|null 
     */ 
    protected function hydrateColumnInfo($key) 
    { 
        if (isset($this->_cache[$key])) { 
            return $this->_cache[$key]; 
        } 
 
        switch (true) { 
            // NOTE: Most of the times it's a field mapping, so keep it first!!! 
            case isset($this->_rsm->fieldMappings[$key]): 
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]); 
                $fieldName     = $this->_rsm->fieldMappings[$key]; 
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName]; 
                $ownerMap      = $this->_rsm->columnOwnerMap[$key]; 
                $columnInfo    = [ 
                    'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true), 
                    'fieldName'    => $fieldName, 
                    'type'         => Type::getType($fieldMapping['type']), 
                    'dqlAlias'     => $ownerMap, 
                    'enumType'     => $this->_rsm->enumMappings[$key] ?? null, 
                ]; 
 
                // the current discriminator value must be saved in order to disambiguate fields hydration, 
                // should there be field name collisions 
                if ($classMetadata->parentClasses && isset($this->_rsm->discriminatorColumns[$ownerMap])) { 
                    return $this->_cache[$key] = array_merge( 
                        $columnInfo, 
                        [ 
                            'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap], 
                            'discriminatorValue'  => $classMetadata->discriminatorValue, 
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata), 
                        ] 
                    ); 
                } 
 
                return $this->_cache[$key] = $columnInfo; 
 
            case isset($this->_rsm->newObjectMappings[$key]): 
                // WARNING: A NEW object is also a scalar, so it must be declared before! 
                $mapping = $this->_rsm->newObjectMappings[$key]; 
 
                return $this->_cache[$key] = [ 
                    'isScalar'             => true, 
                    'isNewObjectParameter' => true, 
                    'fieldName'            => $this->_rsm->scalarMappings[$key], 
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]), 
                    'argIndex'             => $mapping['argIndex'], 
                    'objIndex'             => $mapping['objIndex'], 
                    'class'                => new ReflectionClass($mapping['className']), 
                    'enumType'             => $this->_rsm->enumMappings[$key] ?? null, 
                ]; 
 
            case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]): 
                return $this->_cache[$key] = [ 
                    'fieldName' => $this->_rsm->scalarMappings[$key], 
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]), 
                    'dqlAlias'  => '', 
                    'enumType'  => $this->_rsm->enumMappings[$key] ?? null, 
                ]; 
 
            case isset($this->_rsm->scalarMappings[$key]): 
                return $this->_cache[$key] = [ 
                    'isScalar'  => true, 
                    'fieldName' => $this->_rsm->scalarMappings[$key], 
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]), 
                    'enumType'  => $this->_rsm->enumMappings[$key] ?? null, 
                ]; 
 
            case isset($this->_rsm->metaMappings[$key]): 
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). 
                $fieldName = $this->_rsm->metaMappings[$key]; 
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key]; 
                $type      = isset($this->_rsm->typeMappings[$key]) 
                    ? Type::getType($this->_rsm->typeMappings[$key]) 
                    : null; 
 
                // Cache metadata fetch 
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]); 
 
                return $this->_cache[$key] = [ 
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]), 
                    'isMetaColumn' => true, 
                    'fieldName'    => $fieldName, 
                    'type'         => $type, 
                    'dqlAlias'     => $dqlAlias, 
                    'enumType'     => $this->_rsm->enumMappings[$key] ?? null, 
                ]; 
        } 
 
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. 
        return null; 
    } 
 
    /** 
     * @return string[] 
     * @psalm-return non-empty-list<string> 
     */ 
    private function getDiscriminatorValues(ClassMetadata $classMetadata): array 
    { 
        $values = array_map( 
            function (string $subClass): string { 
                return (string) $this->getClassMetadata($subClass)->discriminatorValue; 
            }, 
            $classMetadata->subClasses 
        ); 
 
        $values[] = (string) $classMetadata->discriminatorValue; 
 
        return $values; 
    } 
 
    /** 
     * Retrieve ClassMetadata associated to entity class name. 
     * 
     * @param string $className 
     * 
     * @return ClassMetadata 
     */ 
    protected function getClassMetadata($className) 
    { 
        if (! isset($this->_metadataCache[$className])) { 
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className); 
        } 
 
        return $this->_metadataCache[$className]; 
    } 
 
    /** 
     * Register entity as managed in UnitOfWork. 
     * 
     * @param object  $entity 
     * @param mixed[] $data 
     * 
     * @return void 
     * 
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow 
     */ 
    protected function registerManaged(ClassMetadata $class, $entity, array $data) 
    { 
        if ($class->isIdentifierComposite) { 
            $id = []; 
 
            foreach ($class->identifier as $fieldName) { 
                $id[$fieldName] = isset($class->associationMappings[$fieldName]) 
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] 
                    : $data[$fieldName]; 
            } 
        } else { 
            $fieldName = $class->identifier[0]; 
            $id        = [ 
                $fieldName => isset($class->associationMappings[$fieldName]) 
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] 
                    : $data[$fieldName], 
            ]; 
        } 
 
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); 
    } 
 
    /** 
     * @param mixed                    $value 
     * @param class-string<BackedEnum> $enumType 
     * 
     * @return BackedEnum|array<BackedEnum> 
     */ 
    final protected function buildEnum($value, string $enumType) 
    { 
        if (is_array($value)) { 
            return array_map(static function ($value) use ($enumType): BackedEnum { 
                return $enumType::from($value); 
            }, $value); 
        } 
 
        return $enumType::from($value); 
    } 
}