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 | 676 | public function __construct(DocumentManager $dm, UnitOfWork $uow) |
|
59 | { |
||
60 | 676 | $this->dm = $dm; |
|
61 | 676 | $this->uow = $uow; |
|
62 | 676 | } |
|
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 | 479 | public function prepareInsertData($document) |
|
71 | { |
||
72 | 479 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
73 | 479 | $changeset = $this->uow->getDocumentChangeSet($document); |
|
74 | |||
75 | 479 | $insertData = array(); |
|
76 | 479 | foreach ($class->fieldMappings as $mapping) { |
|
77 | |||
78 | 479 | $new = isset($changeset[$mapping['fieldName']][1]) ? $changeset[$mapping['fieldName']][1] : null; |
|
79 | |||
80 | 479 | if ($new === null && $mapping['nullable']) { |
|
81 | 145 | $insertData[$mapping['name']] = null; |
|
82 | 145 | } |
|
83 | |||
84 | /* Nothing more to do for null values, since we're either storing |
||
85 | * them (if nullable was true) or not. |
||
86 | */ |
||
87 | 479 | if ($new === null) { |
|
88 | 334 | continue; |
|
89 | } |
||
90 | |||
91 | // @Field, @String, @Date, etc. |
||
92 | 479 | if ( ! isset($mapping['association'])) { |
|
93 | 479 | $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new); |
|
94 | |||
95 | // @ReferenceOne |
||
96 | 479 | } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) { |
|
97 | 78 | $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new); |
|
98 | |||
99 | // @EmbedOne |
||
100 | 391 | } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) { |
|
101 | 62 | $insertData[$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new); |
|
102 | |||
103 | // @ReferenceMany, @EmbedMany |
||
104 | // We're excluding collections using addToSet since there is a risk |
||
105 | // of duplicated entries stored in the collection |
||
106 | 367 | } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide'] |
|
107 | 345 | && $mapping['strategy'] !== 'addToSet' && ! $new->isEmpty()) { |
|
108 | 191 | $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true); |
|
109 | 191 | } |
|
110 | 479 | } |
|
111 | |||
112 | // add discriminator if the class has one |
||
113 | 478 | View Code Duplication | if (isset($class->discriminatorField)) { |
|
|||
114 | 32 | $insertData[$class->discriminatorField] = isset($class->discriminatorValue) |
|
115 | 32 | ? $class->discriminatorValue |
|
116 | 32 | : $class->name; |
|
117 | 32 | } |
|
118 | |||
119 | 478 | return $insertData; |
|
120 | } |
||
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 | 210 | public function prepareUpdateData($document) |
|
129 | { |
||
130 | 210 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
131 | 210 | $changeset = $this->uow->getDocumentChangeSet($document); |
|
132 | |||
133 | 210 | $updateData = array(); |
|
134 | 210 | foreach ($changeset as $fieldName => $change) { |
|
135 | 171 | $mapping = $class->fieldMappings[$fieldName]; |
|
136 | |||
137 | // skip non embedded document identifiers |
||
138 | 171 | if ( ! $class->isEmbeddedDocument && ! empty($mapping['id'])) { |
|
139 | 1 | continue; |
|
140 | } |
||
141 | |||
142 | 171 | list($old, $new) = $change; |
|
143 | |||
144 | // @Inc |
||
145 | 171 | if ($mapping['type'] === 'increment') { |
|
146 | 4 | if ($new === null) { |
|
147 | 1 | if ($mapping['nullable'] === true) { |
|
148 | $updateData['$set'][$mapping['name']] = null; |
||
149 | } else { |
||
150 | 1 | $updateData['$unset'][$mapping['name']] = true; |
|
151 | } |
||
152 | 4 | } elseif ($new >= $old) { |
|
153 | 4 | $updateData['$inc'][$mapping['name']] = $new - $old; |
|
154 | 4 | } else { |
|
155 | 1 | $updateData['$inc'][$mapping['name']] = ($old - $new) * -1; |
|
156 | } |
||
157 | |||
158 | // @Field, @String, @Date, etc. |
||
159 | 171 | } elseif ( ! isset($mapping['association'])) { |
|
160 | 112 | if (isset($new) || $mapping['nullable'] === true) { |
|
161 | 112 | $updateData['$set'][$mapping['name']] = (is_null($new) ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new)); |
|
162 | 112 | } else { |
|
163 | $updateData['$unset'][$mapping['name']] = true; |
||
164 | } |
||
165 | |||
166 | // @EmbedOne |
||
167 | 168 | } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) { |
|
168 | // If we have a new embedded document then lets set the whole thing |
||
169 | 24 | if ($new && $this->uow->isScheduledForInsert($new)) { |
|
170 | 8 | $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new); |
|
171 | |||
172 | // If we don't have a new value then lets unset the embedded document |
||
173 | 24 | } elseif ( ! $new) { |
|
174 | 3 | $updateData['$unset'][$mapping['name']] = true; |
|
175 | |||
176 | // Update existing embedded document |
||
177 | 3 | View Code Duplication | } else { |
178 | 16 | $update = $this->prepareUpdateData($new); |
|
179 | 16 | foreach ($update as $cmd => $values) { |
|
180 | 14 | foreach ($values as $key => $value) { |
|
181 | 14 | $updateData[$cmd][$mapping['name'] . '.' . $key] = $value; |
|
182 | 14 | } |
|
183 | 16 | } |
|
184 | } |
||
185 | |||
186 | // @ReferenceMany, @EmbedMany |
||
187 | 93 | } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) { |
|
188 | 64 | if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) { |
|
189 | 12 | $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true); |
|
190 | 64 | View Code Duplication | } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) { |
191 | $updateData['$unset'][$mapping['name']] = true; |
||
192 | $this->uow->unscheduleCollectionDeletion($new); |
||
193 | 52 | } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) { |
|
194 | 2 | $updateData['$unset'][$mapping['name']] = true; |
|
195 | 2 | $this->uow->unscheduleCollectionDeletion($old); |
|
196 | 52 | } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) { |
|
197 | 41 | foreach ($new as $key => $embeddedDoc) { |
|
198 | 39 | if ( ! $this->uow->isScheduledForInsert($embeddedDoc)) { |
|
199 | 28 | $update = $this->prepareUpdateData($embeddedDoc); |
|
200 | 28 | foreach ($update as $cmd => $values) { |
|
201 | 14 | foreach ($values as $name => $value) { |
|
202 | 14 | $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value; |
|
203 | 14 | } |
|
204 | 28 | } |
|
205 | 28 | } |
|
206 | 41 | } |
|
207 | 41 | } |
|
208 | |||
209 | // @ReferenceOne |
||
210 | 75 | } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) { |
|
211 | 10 | View Code Duplication | if (isset($new) || $mapping['nullable'] === true) { |
212 | 10 | $updateData['$set'][$mapping['name']] = (is_null($new) ? null : $this->prepareReferencedDocumentValue($mapping, $new)); |
|
213 | 10 | } else { |
|
214 | 2 | $updateData['$unset'][$mapping['name']] = true; |
|
215 | } |
||
216 | 10 | } |
|
217 | 210 | } |
|
218 | // collections that aren't dirty but could be subject to update are |
||
219 | // excluded from change set, let's go through them now |
||
220 | 210 | foreach ($this->uow->getScheduledCollections($document) as $coll) { |
|
221 | 98 | $mapping = $coll->getMapping(); |
|
222 | 98 | if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) { |
|
223 | 10 | $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true); |
|
224 | 98 | View Code Duplication | } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($coll)) { |
225 | 2 | $updateData['$unset'][$mapping['name']] = true; |
|
226 | 2 | $this->uow->unscheduleCollectionDeletion($coll); |
|
227 | 2 | } |
|
228 | // @ReferenceMany is handled by CollectionPersister |
||
229 | 210 | } |
|
230 | 210 | return $updateData; |
|
231 | } |
||
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) |
|
240 | { |
||
241 | 77 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
242 | 77 | $changeset = $this->uow->getDocumentChangeSet($document); |
|
243 | |||
244 | 77 | $updateData = array(); |
|
245 | 77 | foreach ($changeset as $fieldName => $change) { |
|
246 | 77 | $mapping = $class->fieldMappings[$fieldName]; |
|
247 | |||
248 | 77 | list($old, $new) = $change; |
|
249 | |||
250 | // @Inc |
||
251 | 77 | if ($mapping['type'] === 'increment') { |
|
252 | 4 | if ($new >= $old) { |
|
253 | 4 | $updateData['$inc'][$mapping['name']] = $new - $old; |
|
254 | 4 | } else { |
|
255 | $updateData['$inc'][$mapping['name']] = ($old - $new) * -1; |
||
256 | } |
||
257 | |||
258 | // @Field, @String, @Date, etc. |
||
259 | 77 | } elseif ( ! isset($mapping['association'])) { |
|
260 | 77 | if (isset($new) || $mapping['nullable'] === true) { |
|
261 | 77 | $updateData['$set'][$mapping['name']] = (is_null($new) ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new)); |
|
262 | 77 | } |
|
263 | |||
264 | // @EmbedOne |
||
265 | 77 | } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) { |
|
266 | // If we have a new embedded document then lets set the whole thing |
||
267 | 3 | if ($new && $this->uow->isScheduledForInsert($new)) { |
|
268 | 1 | $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new); |
|
269 | |||
270 | // If we don't have a new value then do nothing on upsert |
||
271 | 3 | } elseif ( ! $new) { |
|
272 | |||
273 | // Update existing embedded document |
||
274 | 2 | View Code Duplication | } else { |
275 | $update = $this->prepareUpsertData($new); |
||
276 | foreach ($update as $cmd => $values) { |
||
277 | foreach ($values as $key => $value) { |
||
278 | $updateData[$cmd][$mapping['name'] . '.' . $key] = $value; |
||
279 | } |
||
280 | } |
||
281 | } |
||
282 | |||
283 | // @ReferenceOne |
||
284 | 22 | View Code Duplication | } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) { |
285 | 12 | if (isset($new) || $mapping['nullable'] === true) { |
|
286 | 9 | $updateData['$set'][$mapping['name']] = (is_null($new) ? null : $this->prepareReferencedDocumentValue($mapping, $new)); |
|
287 | 9 | } |
|
288 | |||
289 | // @ReferenceMany, @EmbedMany |
||
290 | 21 | } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide'] |
|
291 | 13 | && $new instanceof PersistentCollection && $new->isDirty() |
|
292 | 13 | && CollectionHelper::isAtomic($mapping['strategy'])) { |
|
293 | 1 | $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true); |
|
294 | 1 | } |
|
295 | // @EmbedMany and @ReferenceMany are handled by CollectionPersister |
||
296 | 77 | } |
|
297 | |||
298 | // add discriminator if the class has one |
||
299 | 77 | View Code Duplication | if (isset($class->discriminatorField)) { |
300 | 5 | $updateData['$set'][$class->discriminatorField] = isset($class->discriminatorValue) |
|
301 | 5 | ? $class->discriminatorValue |
|
302 | 5 | : $class->name; |
|
303 | 5 | } |
|
304 | |||
305 | 77 | return $updateData; |
|
306 | } |
||
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 | 187 | 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 | 170 | public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false) |
|
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 | 20 | 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 | 207 | 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.