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 PersistenceBuilder 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 PersistenceBuilder, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
36 | class PersistenceBuilder |
||
37 | { |
||
38 | /** |
||
39 | * The DocumentManager instance. |
||
40 | * |
||
41 | * @var DocumentManager |
||
42 | */ |
||
43 | private $dm; |
||
44 | |||
45 | /** |
||
46 | * The UnitOfWork instance. |
||
47 | * |
||
48 | * @var UnitOfWork |
||
49 | */ |
||
50 | private $uow; |
||
51 | |||
52 | /** |
||
53 | * Initializes a new PersistenceBuilder instance. |
||
54 | * |
||
55 | * @param DocumentManager $dm |
||
56 | * @param UnitOfWork $uow |
||
57 | */ |
||
58 | 683 | public function __construct(DocumentManager $dm, UnitOfWork $uow) |
|
63 | |||
64 | /** |
||
65 | * Prepares the array that is ready to be inserted to mongodb for a given object document. |
||
66 | * |
||
67 | * @param object $document |
||
68 | * @return array $insertData |
||
69 | */ |
||
70 | 486 | public function prepareInsertData($document) |
|
121 | |||
122 | /** |
||
123 | * Prepares the update query to update a given document object in mongodb. |
||
124 | * |
||
125 | * @param object $document |
||
126 | * @return array $updateData |
||
127 | */ |
||
128 | 213 | public function prepareUpdateData($document) |
|
232 | |||
233 | /** |
||
234 | * Prepares the update query to upsert a given document object in mongodb. |
||
235 | * |
||
236 | * @param object $document |
||
237 | * @return array $updateData |
||
238 | */ |
||
239 | 77 | public function prepareUpsertData($document) |
|
307 | |||
308 | /** |
||
309 | * Returns the reference representation to be stored in MongoDB. |
||
310 | * |
||
311 | * If the document does not have an identifier and the mapping calls for a |
||
312 | * simple reference, null may be returned. |
||
313 | * |
||
314 | * @param array $referenceMapping |
||
315 | * @param object $document |
||
316 | * @return array|null |
||
317 | */ |
||
318 | 190 | public function prepareReferencedDocumentValue(array $referenceMapping, $document) |
|
322 | |||
323 | /** |
||
324 | * Returns the embedded document to be stored in MongoDB. |
||
325 | * |
||
326 | * The return value will usually be an associative array with string keys |
||
327 | * corresponding to field names on the embedded document. An object may be |
||
328 | * returned if the document is empty, to ensure that a BSON object will be |
||
329 | * stored in lieu of an array. |
||
330 | * |
||
331 | * If $includeNestedCollections is true, nested collections will be included |
||
332 | * in this prepared value and the option will cascade to all embedded |
||
333 | * associations. If any nested PersistentCollections (embed or reference) |
||
334 | * within this value were previously scheduled for deletion or update, they |
||
335 | * will also be unscheduled. |
||
336 | * |
||
337 | * @param array $embeddedMapping |
||
338 | * @param object $embeddedDocument |
||
339 | * @param boolean $includeNestedCollections |
||
340 | * @return array|object |
||
341 | * @throws \UnexpectedValueException if an unsupported associating mapping is found |
||
342 | */ |
||
343 | 173 | public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false) |
|
344 | { |
||
345 | 173 | $embeddedDocumentValue = array(); |
|
346 | 173 | $class = $this->dm->getClassMetadata(get_class($embeddedDocument)); |
|
347 | |||
348 | 173 | foreach ($class->fieldMappings as $mapping) { |
|
349 | // Skip notSaved fields |
||
350 | 171 | if ( ! empty($mapping['notSaved'])) { |
|
351 | 1 | continue; |
|
352 | } |
||
353 | |||
354 | // Inline ClassMetadataInfo::getFieldValue() |
||
355 | 171 | $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument); |
|
356 | |||
357 | 171 | $value = null; |
|
358 | |||
359 | 171 | if ($rawValue !== null) { |
|
360 | 168 | switch (isset($mapping['association']) ? $mapping['association'] : null) { |
|
361 | // @Field, @String, @Date, etc. |
||
362 | 168 | case null: |
|
363 | 163 | $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue); |
|
364 | 163 | break; |
|
365 | |||
366 | 64 | case ClassMetadata::EMBED_ONE: |
|
367 | 64 | case ClassMetadata::REFERENCE_ONE: |
|
368 | // Nested collections should only be included for embedded relationships |
||
369 | 21 | $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded'])); |
|
370 | 21 | break; |
|
371 | |||
372 | 44 | case ClassMetadata::EMBED_MANY: |
|
373 | 44 | case ClassMetadata::REFERENCE_MANY: |
|
374 | // Skip PersistentCollections already scheduled for deletion |
||
375 | 44 | if ( ! $includeNestedCollections && $rawValue instanceof PersistentCollection |
|
376 | 44 | && $this->uow->isCollectionScheduledForDeletion($rawValue)) { |
|
377 | break; |
||
378 | } |
||
379 | |||
380 | 44 | $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections); |
|
381 | 44 | break; |
|
382 | |||
383 | default: |
||
384 | throw new \UnexpectedValueException('Unsupported mapping association: ' . $mapping['association']); |
||
385 | 168 | } |
|
386 | 168 | } |
|
387 | |||
388 | // Omit non-nullable fields that would have a null value |
||
389 | 171 | if ($value === null && $mapping['nullable'] === false) { |
|
390 | 60 | continue; |
|
391 | } |
||
392 | |||
393 | 168 | $embeddedDocumentValue[$mapping['name']] = $value; |
|
394 | 173 | } |
|
395 | |||
396 | /* Add a discriminator value if the embedded document is not mapped |
||
397 | * explicitly to a targetDocument class. |
||
398 | */ |
||
399 | 173 | View Code Duplication | if ( ! isset($embeddedMapping['targetDocument'])) { |
400 | 16 | $discriminatorField = $embeddedMapping['discriminatorField']; |
|
401 | 16 | $discriminatorValue = isset($embeddedMapping['discriminatorMap']) |
|
402 | 16 | ? array_search($class->name, $embeddedMapping['discriminatorMap']) |
|
403 | 16 | : $class->name; |
|
404 | |||
405 | /* If the discriminator value was not found in the map, use the full |
||
406 | * class name. In the future, it may be preferable to throw an |
||
407 | * exception here (perhaps based on some strictness option). |
||
408 | * |
||
409 | * @see DocumentManager::createDBRef() |
||
410 | */ |
||
411 | 16 | if ($discriminatorValue === false) { |
|
412 | 2 | $discriminatorValue = $class->name; |
|
413 | 2 | } |
|
414 | |||
415 | 16 | $embeddedDocumentValue[$discriminatorField] = $discriminatorValue; |
|
416 | 16 | } |
|
417 | |||
418 | /* If the class has a discriminator (field and value), use it. A child |
||
419 | * class that is not defined in the discriminator map may only have a |
||
420 | * discriminator field and no value, so default to the full class name. |
||
421 | */ |
||
422 | 173 | View Code Duplication | if (isset($class->discriminatorField)) { |
423 | 8 | $embeddedDocumentValue[$class->discriminatorField] = isset($class->discriminatorValue) |
|
424 | 8 | ? $class->discriminatorValue |
|
425 | 8 | : $class->name; |
|
426 | 8 | } |
|
427 | |||
428 | // Ensure empty embedded documents are stored as BSON objects |
||
429 | 173 | if (empty($embeddedDocumentValue)) { |
|
430 | 6 | return (object) $embeddedDocumentValue; |
|
431 | } |
||
432 | |||
433 | /* @todo Consider always casting the return value to an object, or |
||
434 | * building $embeddedDocumentValue as an object instead of an array, to |
||
435 | * handle the edge case where all database field names are sequential, |
||
436 | * numeric keys. |
||
437 | */ |
||
438 | 169 | return $embeddedDocumentValue; |
|
439 | } |
||
440 | |||
441 | /* |
||
442 | * Returns the embedded document or reference representation to be stored. |
||
443 | * |
||
444 | * @param array $mapping |
||
445 | * @param object $document |
||
446 | * @param boolean $includeNestedCollections |
||
447 | * @return array|object|null |
||
448 | * @throws \InvalidArgumentException if the mapping is neither embedded nor reference |
||
449 | */ |
||
450 | 21 | public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false) |
|
462 | |||
463 | /** |
||
464 | * Returns the collection representation to be stored and unschedules it afterwards. |
||
465 | * |
||
466 | * @param PersistentCollection $coll |
||
467 | * @param bool $includeNestedCollections |
||
468 | * @return array |
||
469 | */ |
||
470 | 211 | public function prepareAssociatedCollectionValue(PersistentCollection $coll, $includeNestedCollections = false) |
|
490 | } |
||
491 |
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.