vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php line 276

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