Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ObjectHydrator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ObjectHydrator, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
40 | class ObjectHydrator extends AbstractHydrator |
||
41 | { |
||
42 | /** |
||
43 | * @var array |
||
44 | */ |
||
45 | private $identifierMap = []; |
||
46 | |||
47 | /** |
||
48 | * @var array |
||
49 | */ |
||
50 | private $resultPointers = []; |
||
51 | |||
52 | /** |
||
53 | * @var array |
||
54 | */ |
||
55 | private $idTemplate = []; |
||
56 | |||
57 | /** |
||
58 | * @var integer |
||
59 | */ |
||
60 | private $resultCounter = 0; |
||
61 | |||
62 | /** |
||
63 | * @var array |
||
64 | */ |
||
65 | private $rootAliases = []; |
||
66 | |||
67 | /** |
||
68 | * @var array |
||
69 | */ |
||
70 | private $initializedCollections = []; |
||
71 | |||
72 | /** |
||
73 | * @var array |
||
74 | */ |
||
75 | private $existingCollections = []; |
||
76 | |||
77 | /** |
||
78 | * {@inheritdoc} |
||
79 | */ |
||
80 | 673 | protected function prepare() |
|
132 | |||
133 | /** |
||
134 | * {@inheritdoc} |
||
135 | */ |
||
136 | 667 | protected function cleanup() |
|
153 | |||
154 | /** |
||
155 | * {@inheritdoc} |
||
156 | */ |
||
157 | 666 | View Code Duplication | protected function hydrateAllData() |
158 | { |
||
159 | 666 | $result = []; |
|
160 | |||
161 | 666 | while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { |
|
162 | 628 | $this->hydrateRowData($row, $result); |
|
163 | } |
||
164 | |||
165 | // Take snapshots from all newly initialized collections |
||
166 | 663 | foreach ($this->initializedCollections as $coll) { |
|
167 | 69 | $coll->takeSnapshot(); |
|
168 | } |
||
169 | |||
170 | 663 | return $result; |
|
171 | } |
||
172 | |||
173 | /** |
||
174 | * Initializes a related collection. |
||
175 | * |
||
176 | * @param object $entity The entity to which the collection belongs. |
||
177 | * @param ClassMetadata $class |
||
178 | * @param string $fieldName The name of the field on the entity that holds the collection. |
||
179 | * @param string $parentDqlAlias Alias of the parent fetch joining this collection. |
||
180 | * |
||
181 | * @return \Doctrine\ORM\PersistentCollection |
||
182 | */ |
||
183 | 101 | private function initRelatedCollection($entity, $class, $fieldName, $parentDqlAlias) |
|
221 | |||
222 | /** |
||
223 | * Gets an entity instance. |
||
224 | * |
||
225 | * @param array $data The instance data. |
||
226 | * @param string $dqlAlias The DQL alias of the entity's class. |
||
227 | * |
||
228 | * @return object The entity. |
||
229 | * |
||
230 | * @throws HydrationException |
||
231 | */ |
||
232 | 598 | private function getEntity(array $data, $dqlAlias) |
|
273 | |||
274 | /** |
||
275 | * @param string $className |
||
276 | * @param array $data |
||
277 | * |
||
278 | * @return mixed |
||
279 | */ |
||
280 | 34 | private function getEntityFromIdentityMap($className, array $data) |
|
281 | { |
||
282 | // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? |
||
283 | 34 | $class = $this->_metadataCache[$className]; |
|
284 | |||
285 | /* @var $class ClassMetadata */ |
||
286 | 34 | if ($class->isIdentifierComposite) { |
|
287 | 1 | $idHash = ''; |
|
288 | |||
289 | 1 | View Code Duplication | foreach ($class->identifier as $fieldName) { |
|
|||
290 | 1 | $idHash .= ' ' . (isset($class->associationMappings[$fieldName]) |
|
291 | 1 | ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] |
|
292 | 1 | : $data[$fieldName]); |
|
293 | } |
||
294 | |||
295 | 1 | return $this->_uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName); |
|
296 | 33 | } else if (isset($class->associationMappings[$class->identifier[0]])) { |
|
297 | return $this->_uow->tryGetByIdHash($data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']], $class->rootEntityName); |
||
298 | } |
||
299 | |||
300 | 33 | return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); |
|
301 | } |
||
302 | |||
303 | /** |
||
304 | * Hydrates a single row in an SQL result set. |
||
305 | * |
||
306 | * @internal |
||
307 | * First, the data of the row is split into chunks where each chunk contains data |
||
308 | * that belongs to a particular component/class. Afterwards, all these chunks |
||
309 | * are processed, one after the other. For each chunk of class data only one of the |
||
310 | * following code paths is executed: |
||
311 | * |
||
312 | * Path A: The data chunk belongs to a joined/associated object and the association |
||
313 | * is collection-valued. |
||
314 | * Path B: The data chunk belongs to a joined/associated object and the association |
||
315 | * is single-valued. |
||
316 | * Path C: The data chunk belongs to a root result element/object that appears in the topmost |
||
317 | * level of the hydrated result. A typical example are the objects of the type |
||
318 | * specified by the FROM clause in a DQL query. |
||
319 | * |
||
320 | * @param array $row The data of the row to process. |
||
321 | * @param array $result The result array to fill. |
||
322 | * |
||
323 | * @return void |
||
324 | */ |
||
325 | 633 | protected function hydrateRowData(array $row, array &$result) |
|
326 | { |
||
327 | // Initialize |
||
328 | 633 | $id = $this->idTemplate; // initialize the id-memory |
|
329 | 633 | $nonemptyComponents = []; |
|
330 | // Split the row data into chunks of class data. |
||
331 | 633 | $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); |
|
332 | |||
333 | // reset result pointers for each data row |
||
334 | 633 | $this->resultPointers = []; |
|
335 | |||
336 | // Hydrate the data chunks |
||
337 | 633 | foreach ($rowData['data'] as $dqlAlias => $data) { |
|
338 | 598 | $entityName = $this->_rsm->aliasMap[$dqlAlias]; |
|
339 | |||
340 | 598 | if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { |
|
341 | // It's a joined result |
||
342 | |||
343 | 317 | $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias]; |
|
344 | // we need the $path to save into the identifier map which entities were already |
||
345 | // seen for this parent-child relationship |
||
346 | 317 | $path = $parentAlias . '.' . $dqlAlias; |
|
347 | |||
348 | // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs |
||
349 | 317 | if ( ! isset($nonemptyComponents[$parentAlias])) { |
|
350 | // TODO: Add special case code where we hydrate the right join objects into identity map at least |
||
351 | 2 | continue; |
|
352 | } |
||
353 | |||
354 | 317 | $parentClass = $this->_metadataCache[$this->_rsm->aliasMap[$parentAlias]]; |
|
355 | 317 | $relationField = $this->_rsm->relationMap[$dqlAlias]; |
|
356 | 317 | $relation = $parentClass->associationMappings[$relationField]; |
|
357 | 317 | $reflField = $parentClass->reflFields[$relationField]; |
|
358 | |||
359 | // Get a reference to the parent object to which the joined element belongs. |
||
360 | 317 | if ($this->_rsm->isMixed && isset($this->rootAliases[$parentAlias])) { |
|
361 | 18 | $objectClass = $this->resultPointers[$parentAlias]; |
|
362 | 18 | $parentObject = $objectClass[key($objectClass)]; |
|
363 | 301 | } else if (isset($this->resultPointers[$parentAlias])) { |
|
364 | 301 | $parentObject = $this->resultPointers[$parentAlias]; |
|
365 | } else { |
||
366 | // Parent object of relation not found, mark as not-fetched again |
||
367 | 2 | $element = $this->getEntity($data, $dqlAlias); |
|
368 | |||
369 | // Update result pointer and provide initial fetch data for parent |
||
370 | 2 | $this->resultPointers[$dqlAlias] = $element; |
|
371 | 2 | $rowData['data'][$parentAlias][$relationField] = $element; |
|
372 | |||
373 | // Mark as not-fetched again |
||
374 | 2 | unset($this->_hints['fetched'][$parentAlias][$relationField]); |
|
375 | 2 | continue; |
|
376 | } |
||
377 | |||
378 | 317 | $oid = spl_object_hash($parentObject); |
|
379 | |||
380 | // Check the type of the relation (many or single-valued) |
||
381 | 317 | if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) { |
|
382 | // PATH A: Collection-valued association |
||
383 | 102 | $reflFieldValue = $reflField->getValue($parentObject); |
|
384 | |||
385 | 102 | if (isset($nonemptyComponents[$dqlAlias])) { |
|
386 | 98 | $collKey = $oid . $relationField; |
|
387 | 98 | if (isset($this->initializedCollections[$collKey])) { |
|
388 | 45 | $reflFieldValue = $this->initializedCollections[$collKey]; |
|
389 | 98 | } else if ( ! isset($this->existingCollections[$collKey])) { |
|
390 | 98 | $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); |
|
391 | } |
||
392 | |||
393 | 98 | $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); |
|
394 | 98 | $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; |
|
395 | 98 | $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; |
|
396 | |||
397 | 98 | if ( ! $indexExists || ! $indexIsValid) { |
|
398 | 98 | if (isset($this->existingCollections[$collKey])) { |
|
399 | // Collection exists, only look for the element in the identity map. |
||
400 | 34 | if ($element = $this->getEntityFromIdentityMap($entityName, $data)) { |
|
401 | 34 | $this->resultPointers[$dqlAlias] = $element; |
|
402 | } else { |
||
403 | 34 | unset($this->resultPointers[$dqlAlias]); |
|
404 | } |
||
405 | } else { |
||
406 | 66 | $element = $this->getEntity($data, $dqlAlias); |
|
407 | |||
408 | 66 | if (isset($this->_rsm->indexByMap[$dqlAlias])) { |
|
409 | 11 | $indexValue = $row[$this->_rsm->indexByMap[$dqlAlias]]; |
|
410 | 11 | $reflFieldValue->hydrateSet($indexValue, $element); |
|
411 | 11 | $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; |
|
412 | } else { |
||
413 | 55 | $reflFieldValue->hydrateAdd($element); |
|
414 | 55 | $reflFieldValue->last(); |
|
415 | 55 | $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); |
|
416 | } |
||
417 | // Update result pointer |
||
418 | 98 | $this->resultPointers[$dqlAlias] = $element; |
|
419 | } |
||
420 | } else { |
||
421 | // Update result pointer |
||
422 | 98 | $this->resultPointers[$dqlAlias] = $reflFieldValue[$index]; |
|
423 | } |
||
424 | 9 | } else if ( ! $reflFieldValue) { |
|
425 | 5 | $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); |
|
426 | 6 | } else if ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false) { |
|
427 | 102 | $reflFieldValue->setInitialized(true); |
|
428 | } |
||
429 | |||
430 | } else { |
||
431 | // PATH B: Single-valued association |
||
432 | 236 | $reflFieldValue = $reflField->getValue($parentObject); |
|
433 | |||
434 | 236 | if ( ! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || ($reflFieldValue instanceof Proxy && !$reflFieldValue->__isInitialized__)) { |
|
435 | // we only need to take action if this value is null, |
||
436 | // we refresh the entity or its an uninitialized proxy. |
||
437 | 221 | if (isset($nonemptyComponents[$dqlAlias])) { |
|
438 | 116 | $element = $this->getEntity($data, $dqlAlias); |
|
439 | 115 | $reflField->setValue($parentObject, $element); |
|
440 | 115 | $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); |
|
441 | 115 | $targetClass = $this->_metadataCache[$relation['targetEntity']]; |
|
442 | |||
443 | 115 | if ($relation['isOwningSide']) { |
|
444 | // TODO: Just check hints['fetched'] here? |
||
445 | // If there is an inverse mapping on the target class its bidirectional |
||
446 | 51 | if ($relation['inversedBy']) { |
|
447 | 35 | $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']]; |
|
448 | 35 | View Code Duplication | if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { |
449 | 12 | $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject); |
|
450 | 35 | $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $inverseAssoc['fieldName'], $parentObject); |
|
451 | } |
||
452 | 17 | } else if ($parentClass === $targetClass && $relation['mappedBy']) { |
|
453 | // Special case: bi-directional self-referencing one-one on the same class |
||
454 | 51 | $targetClass->reflFields[$relationField]->setValue($element, $parentObject); |
|
455 | } |
||
456 | View Code Duplication | } else { |
|
457 | // For sure bidirectional, as there is no inverse side in unidirectional mappings |
||
458 | 67 | $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject); |
|
459 | 67 | $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $relation['mappedBy'], $parentObject); |
|
460 | } |
||
461 | // Update result pointer |
||
462 | 115 | $this->resultPointers[$dqlAlias] = $element; |
|
463 | } else { |
||
464 | 125 | $this->_uow->setOriginalEntityProperty($oid, $relationField, null); |
|
465 | 220 | $reflField->setValue($parentObject, null); |
|
466 | } |
||
467 | // else leave $reflFieldValue null for single-valued associations |
||
468 | } else { |
||
469 | // Update result pointer |
||
470 | 316 | $this->resultPointers[$dqlAlias] = $reflFieldValue; |
|
471 | } |
||
472 | } |
||
473 | } else { |
||
474 | // PATH C: Its a root result element |
||
475 | 598 | $this->rootAliases[$dqlAlias] = true; // Mark as root alias |
|
476 | 598 | $entityKey = $this->_rsm->entityMappings[$dqlAlias] ?: 0; |
|
477 | |||
478 | // if this row has a NULL value for the root result id then make it a null result. |
||
479 | 598 | View Code Duplication | if ( ! isset($nonemptyComponents[$dqlAlias]) ) { |
480 | 3 | if ($this->_rsm->isMixed) { |
|
481 | 2 | $result[] = [$entityKey => null]; |
|
482 | } else { |
||
483 | 1 | $result[] = null; |
|
484 | } |
||
485 | 3 | $resultKey = $this->resultCounter; |
|
486 | 3 | ++$this->resultCounter; |
|
487 | 3 | continue; |
|
488 | } |
||
489 | |||
490 | // check for existing result from the iterations before |
||
491 | 598 | if ( ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { |
|
492 | 598 | $element = $this->getEntity($data, $dqlAlias); |
|
493 | |||
494 | 596 | if ($this->_rsm->isMixed) { |
|
495 | 41 | $element = [$entityKey => $element]; |
|
496 | } |
||
497 | |||
498 | 596 | if (isset($this->_rsm->indexByMap[$dqlAlias])) { |
|
499 | 26 | $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]]; |
|
500 | |||
501 | 26 | if (isset($this->_hints['collection'])) { |
|
502 | 10 | $this->_hints['collection']->hydrateSet($resultKey, $element); |
|
503 | } |
||
504 | |||
505 | 26 | $result[$resultKey] = $element; |
|
506 | } else { |
||
507 | 579 | $resultKey = $this->resultCounter; |
|
508 | 579 | ++$this->resultCounter; |
|
509 | |||
510 | 579 | if (isset($this->_hints['collection'])) { |
|
511 | 110 | $this->_hints['collection']->hydrateAdd($element); |
|
512 | } |
||
513 | |||
514 | 579 | $result[] = $element; |
|
515 | } |
||
516 | |||
517 | 596 | $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; |
|
518 | |||
519 | // Update result pointer |
||
520 | 596 | $this->resultPointers[$dqlAlias] = $element; |
|
521 | |||
522 | } else { |
||
523 | // Update result pointer |
||
524 | 76 | $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; |
|
525 | 76 | $this->resultPointers[$dqlAlias] = $result[$index]; |
|
526 | 76 | $resultKey = $index; |
|
527 | } |
||
528 | } |
||
529 | |||
530 | 596 | View Code Duplication | if (isset($this->_hints[Query::HINT_INTERNAL_ITERATION]) && $this->_hints[Query::HINT_INTERNAL_ITERATION]) { |
531 | 596 | $this->_uow->hydrationComplete(); |
|
532 | } |
||
533 | } |
||
534 | |||
535 | 630 | if ( ! isset($resultKey) ) { |
|
536 | 35 | $this->resultCounter++; |
|
537 | } |
||
538 | |||
539 | // Append scalar values to mixed result sets |
||
540 | 630 | View Code Duplication | if (isset($rowData['scalars'])) { |
541 | 54 | if ( ! isset($resultKey) ) { |
|
542 | 22 | $resultKey = (isset($this->_rsm->indexByMap['scalars'])) |
|
543 | 2 | ? $row[$this->_rsm->indexByMap['scalars']] |
|
544 | 22 | : $this->resultCounter - 1; |
|
545 | } |
||
546 | |||
547 | 54 | foreach ($rowData['scalars'] as $name => $value) { |
|
548 | 54 | $result[$resultKey][$name] = $value; |
|
549 | } |
||
550 | } |
||
551 | |||
552 | // Append new object to mixed result sets |
||
553 | 630 | View Code Duplication | if (isset($rowData['newObjects'])) { |
554 | 19 | if ( ! isset($resultKey) ) { |
|
555 | 13 | $resultKey = $this->resultCounter - 1; |
|
556 | } |
||
557 | |||
558 | |||
559 | 19 | $scalarCount = (isset($rowData['scalars'])? count($rowData['scalars']): 0); |
|
560 | |||
561 | 19 | foreach ($rowData['newObjects'] as $objIndex => $newObject) { |
|
562 | 19 | $class = $newObject['class']; |
|
563 | 19 | $args = $newObject['args']; |
|
564 | 19 | $obj = $class->newInstanceArgs($args); |
|
565 | |||
566 | 19 | if ($scalarCount == 0 && count($rowData['newObjects']) == 1 ) { |
|
567 | 10 | $result[$resultKey] = $obj; |
|
568 | |||
569 | 10 | continue; |
|
570 | } |
||
571 | |||
572 | 9 | $result[$resultKey][$objIndex] = $obj; |
|
573 | } |
||
574 | } |
||
575 | 630 | } |
|
576 | |||
577 | /** |
||
578 | * When executed in a hydrate() loop we may have to clear internal state to |
||
579 | * decrease memory consumption. |
||
580 | * |
||
581 | * @param mixed $eventArgs |
||
582 | * |
||
583 | * @return void |
||
584 | */ |
||
585 | 3 | public function onClear($eventArgs) |
|
593 | } |
||
594 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.