Completed
Push — master ( 08b9e1...e0c601 )
by Andreas
13s
created

PersistenceBuilder   F

Complexity

Total Complexity 111

Size/Duplication

Total Lines 442
Duplicated Lines 8.37 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 94.97%

Importance

Changes 0
Metric Value
wmc 111
lcom 1
cbo 6
dl 37
loc 442
ccs 170
cts 179
cp 0.9497
rs 2
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
C prepareInsertData() 0 48 15
F prepareUpdateData() 21 100 42
D prepareUpsertData() 16 65 26
A prepareReferencedDocumentValue() 0 4 1
F prepareEmbeddedDocumentValue() 0 95 20
A prepareAssociatedDocumentValue() 0 12 3
A prepareAssociatedCollectionValue() 0 22 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
9
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
10
use Doctrine\ODM\MongoDB\Types\Type;
11
use Doctrine\ODM\MongoDB\UnitOfWork;
12
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
13
use function array_search;
14
use function array_values;
15
use function get_class;
16
17
/**
18
 * PersistenceBuilder builds the queries used by the persisters to update and insert
19
 * documents when a DocumentManager is flushed. It uses the changeset information in the
20
 * UnitOfWork to build queries using atomic operators like $set, $unset, etc.
21
 *
22
 */
23
class PersistenceBuilder
24
{
25
    /**
26
     * The DocumentManager instance.
27
     *
28
     * @var DocumentManager
29
     */
30
    private $dm;
31
32
    /**
33
     * The UnitOfWork instance.
34
     *
35
     * @var UnitOfWork
36
     */
37
    private $uow;
38
39
    /**
40
     * Initializes a new PersistenceBuilder instance.
41
     *
42
     */
43 1080
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
44
    {
45 1080
        $this->dm = $dm;
46 1080
        $this->uow = $uow;
47 1080
    }
48
49
    /**
50
     * Prepares the array that is ready to be inserted to mongodb for a given object document.
51
     *
52
     * @param object $document
53
     * @return array $insertData
54
     */
55 482
    public function prepareInsertData($document)
56
    {
57 482
        $class = $this->dm->getClassMetadata(get_class($document));
58 482
        $changeset = $this->uow->getDocumentChangeSet($document);
59
60 482
        $insertData = [];
61 482
        foreach ($class->fieldMappings as $mapping) {
62 482
            $new = $changeset[$mapping['fieldName']][1] ?? null;
63
64 482
            if ($new === null && $mapping['nullable']) {
65 153
                $insertData[$mapping['name']] = null;
66
            }
67
68
            /* Nothing more to do for null values, since we're either storing
69
             * them (if nullable was true) or not.
70
             */
71 482
            if ($new === null) {
72 335
                continue;
73
            }
74
75
            // @Field, @String, @Date, etc.
76 482
            if (! isset($mapping['association'])) {
77 482
                $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new);
78
79
            // @ReferenceOne
80 387
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
81 100
                $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
82
83
            // @EmbedOne
84 363
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
85 60
                $insertData[$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
86
87
            // @ReferenceMany, @EmbedMany
88
            // We're excluding collections using addToSet since there is a risk
89
            // of duplicated entries stored in the collection
90 345
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
91 345
                    && $mapping['strategy'] !== ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET && ! $new->isEmpty()) {
92 482
                $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
93
            }
94
        }
95
96
        // add discriminator if the class has one
97 481
        if (isset($class->discriminatorField)) {
98 32
            $insertData[$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
99
        }
100
101 481
        return $insertData;
102
    }
103
104
    /**
105
     * Prepares the update query to update a given document object in mongodb.
106
     *
107
     * @param object $document
108
     * @return array $updateData
109
     */
110 197
    public function prepareUpdateData($document)
111
    {
112 197
        $class = $this->dm->getClassMetadata(get_class($document));
113 197
        $changeset = $this->uow->getDocumentChangeSet($document);
114
115 197
        $updateData = [];
116 197
        foreach ($changeset as $fieldName => $change) {
117 196
            $mapping = $class->fieldMappings[$fieldName];
118
119
            // skip non embedded document identifiers
120 196
            if (! $class->isEmbeddedDocument && ! empty($mapping['id'])) {
121 2
                continue;
122
            }
123
124 195
            list($old, $new) = $change;
125
126
            // Scalar fields
127 195
            if (! isset($mapping['association'])) {
128 104
                if ($new === null && $mapping['nullable'] !== true) {
129 1
                    $updateData['$unset'][$mapping['name']] = true;
130
                } else {
131 104
                    if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
132 4
                        $operator = '$inc';
133 4
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
134
                    } else {
135 101
                        $operator = '$set';
136 101
                        $value = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
137
                    }
138
139 104
                    $updateData[$operator][$mapping['name']] = $value;
140
                }
141
142
            // @EmbedOne
143 127
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
144
                // If we have a new embedded document then lets set the whole thing
145 28
                if ($new && $this->uow->isScheduledForInsert($new)) {
146 10
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
147
148
                // If we don't have a new value then lets unset the embedded document
149 21
                } elseif (! $new) {
150 3
                    $updateData['$unset'][$mapping['name']] = true;
151
152
                // Update existing embedded document
153 View Code Duplication
                } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
154 18
                    $update = $this->prepareUpdateData($new);
155 18
                    foreach ($update as $cmd => $values) {
156 14
                        foreach ($values as $key => $value) {
157 28
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
158
                        }
159
                    }
160
                }
161
162
            // @ReferenceMany, @EmbedMany
163 110
            } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) {
164 102
                if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) {
165 8
                    $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
166 95 View Code Duplication
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
167 1
                    $updateData['$unset'][$mapping['name']] = true;
168 1
                    $this->uow->unscheduleCollectionDeletion($new);
169 94
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
170
                    $updateData['$unset'][$mapping['name']] = true;
171
                    $this->uow->unscheduleCollectionDeletion($old);
172 94
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
173 56
                    foreach ($new as $key => $embeddedDoc) {
174 49
                        if ($this->uow->isScheduledForInsert($embeddedDoc)) {
175 34
                            continue;
176
                        }
177
178 38
                        $update = $this->prepareUpdateData($embeddedDoc);
179 38
                        foreach ($update as $cmd => $values) {
180 13
                            foreach ($values as $name => $value) {
181 102
                                $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value;
182
                            }
183
                        }
184
                    }
185
                }
186
187
            // @ReferenceOne
188 14
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
189 11 View Code Duplication
                if (isset($new) || $mapping['nullable'] === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
190 11
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
191
                } else {
192 195
                    $updateData['$unset'][$mapping['name']] = true;
193
                }
194
            }
195
        }
196
        // collections that aren't dirty but could be subject to update are
197
        // excluded from change set, let's go through them now
198 197
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
199 94
            $mapping = $coll->getMapping();
200 94
            if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) {
201 2
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true);
202 92 View Code Duplication
            } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($coll)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
203
                $updateData['$unset'][$mapping['name']] = true;
204 94
                $this->uow->unscheduleCollectionDeletion($coll);
205
            }
206
            // @ReferenceMany is handled by CollectionPersister
207
        }
208 197
        return $updateData;
209
    }
210
211
    /**
212
     * Prepares the update query to upsert a given document object in mongodb.
213
     *
214
     * @param object $document
215
     * @return array $updateData
216
     */
217 84
    public function prepareUpsertData($document)
218
    {
219 84
        $class = $this->dm->getClassMetadata(get_class($document));
220 84
        $changeset = $this->uow->getDocumentChangeSet($document);
221
222 84
        $updateData = [];
223 84
        foreach ($changeset as $fieldName => $change) {
224 84
            $mapping = $class->fieldMappings[$fieldName];
225
226 84
            list($old, $new) = $change;
227
228
            // Scalar fields
229 84
            if (! isset($mapping['association'])) {
230 84
                if ($new !== null) {
231 84
                    if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
232 3
                        $operator = '$inc';
233 3
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
234
                    } else {
235 84
                        $operator = '$set';
236 84
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($new);
237
                    }
238
239 84
                    $updateData[$operator][$mapping['name']] = $value;
240 10
                } elseif ($mapping['nullable'] === true) {
241 84
                    $updateData['$setOnInsert'][$mapping['name']] = null;
242
                }
243
244
            // @EmbedOne
245 25
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
246
                // If we don't have a new value then do nothing on upsert
247
                // If we have a new embedded document then lets set the whole thing
248 8
                if ($new && $this->uow->isScheduledForInsert($new)) {
249 5
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
250 3 View Code Duplication
                } elseif ($new) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
251
                    // Update existing embedded document
252
                    $update = $this->prepareUpsertData($new);
253
                    foreach ($update as $cmd => $values) {
254
                        foreach ($values as $key => $value) {
255 8
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
256
                        }
257
                    }
258
                }
259
260
            // @ReferenceOne
261 22 View Code Duplication
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
262 12
                if (isset($new) || $mapping['nullable'] === true) {
263 12
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
264
                }
265
266
            // @ReferenceMany, @EmbedMany
267 14
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
268 14
                    && $new instanceof PersistentCollectionInterface && $new->isDirty()
269 14
                    && CollectionHelper::isAtomic($mapping['strategy'])) {
270 84
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
271
            }
272
            // @EmbedMany and @ReferenceMany are handled by CollectionPersister
273
        }
274
275
        // add discriminator if the class has one
276 84
        if (isset($class->discriminatorField)) {
277 5
            $updateData['$set'][$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
278
        }
279
280 84
        return $updateData;
281
    }
282
283
    /**
284
     * Returns the reference representation to be stored in MongoDB.
285
     *
286
     * If the document does not have an identifier and the mapping calls for a
287
     * simple reference, null may be returned.
288
     *
289
     * @param array  $referenceMapping
290
     * @param object $document
291
     * @return array|null
292
     */
293 210
    public function prepareReferencedDocumentValue(array $referenceMapping, $document)
294
    {
295 210
        return $this->dm->createReference($document, $referenceMapping);
296
    }
297
298
    /**
299
     * Returns the embedded document to be stored in MongoDB.
300
     *
301
     * The return value will usually be an associative array with string keys
302
     * corresponding to field names on the embedded document. An object may be
303
     * returned if the document is empty, to ensure that a BSON object will be
304
     * stored in lieu of an array.
305
     *
306
     * If $includeNestedCollections is true, nested collections will be included
307
     * in this prepared value and the option will cascade to all embedded
308
     * associations. If any nested PersistentCollections (embed or reference)
309
     * within this value were previously scheduled for deletion or update, they
310
     * will also be unscheduled.
311
     *
312
     * @param array  $embeddedMapping
313
     * @param object $embeddedDocument
314
     * @param bool   $includeNestedCollections
315
     * @return array|object
316
     * @throws \UnexpectedValueException If an unsupported associating mapping is found.
317
     */
318 159
    public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false)
319
    {
320 159
        $embeddedDocumentValue = [];
321 159
        $class = $this->dm->getClassMetadata(get_class($embeddedDocument));
322
323 159
        foreach ($class->fieldMappings as $mapping) {
324
            // Skip notSaved fields
325 157
            if (! empty($mapping['notSaved'])) {
326 1
                continue;
327
            }
328
329
            // Inline ClassMetadata::getFieldValue()
330 157
            $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument);
331
332 157
            $value = null;
333
334 157
            if ($rawValue !== null) {
335 154
                switch ($mapping['association'] ?? null) {
336
                    // @Field, @String, @Date, etc.
337
                    case null:
338 148
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
339 148
                        break;
340
341 55
                    case ClassMetadata::EMBED_ONE:
342 52
                    case ClassMetadata::REFERENCE_ONE:
343
                        // Nested collections should only be included for embedded relationships
344 20
                        $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded']));
345 20
                        break;
346
347 36
                    case ClassMetadata::EMBED_MANY:
348 4
                    case ClassMetadata::REFERENCE_MANY:
349
                        // Skip PersistentCollections already scheduled for deletion
350 36
                        if (! $includeNestedCollections && $rawValue instanceof PersistentCollectionInterface
351 36
                            && $this->uow->isCollectionScheduledForDeletion($rawValue)) {
352
                            break;
353
                        }
354
355 36
                        $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
356 36
                        break;
357
358
                    default:
359
                        throw new \UnexpectedValueException('Unsupported mapping association: ' . $mapping['association']);
360
                }
361
            }
362
363
            // Omit non-nullable fields that would have a null value
364 157
            if ($value === null && $mapping['nullable'] === false) {
365 47
                continue;
366
            }
367
368 154
            $embeddedDocumentValue[$mapping['name']] = $value;
369
        }
370
371
        /* Add a discriminator value if the embedded document is not mapped
372
         * explicitly to a targetDocument class.
373
         */
374 159
        if (! isset($embeddedMapping['targetDocument'])) {
375 16
            $discriminatorField = $embeddedMapping['discriminatorField'];
376 16
            $discriminatorValue = isset($embeddedMapping['discriminatorMap'])
377 5
                ? array_search($class->name, $embeddedMapping['discriminatorMap'])
378 16
                : $class->name;
379
380
            /* If the discriminator value was not found in the map, use the full
381
             * class name. In the future, it may be preferable to throw an
382
             * exception here (perhaps based on some strictness option).
383
             *
384
             * @see DocumentManager::createDBRef()
385
             */
386 16
            if ($discriminatorValue === false) {
387 2
                $discriminatorValue = $class->name;
388
            }
389
390 16
            $embeddedDocumentValue[$discriminatorField] = $discriminatorValue;
391
        }
392
393
        /* If the class has a discriminator (field and value), use it. A child
394
         * class that is not defined in the discriminator map may only have a
395
         * discriminator field and no value, so default to the full class name.
396
         */
397 159
        if (isset($class->discriminatorField)) {
398 8
            $embeddedDocumentValue[$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
399
        }
400
401
        // Ensure empty embedded documents are stored as BSON objects
402 159
        if (empty($embeddedDocumentValue)) {
403 6
            return (object) $embeddedDocumentValue;
404
        }
405
406
        /* @todo Consider always casting the return value to an object, or
407
         * building $embeddedDocumentValue as an object instead of an array, to
408
         * handle the edge case where all database field names are sequential,
409
         * numeric keys.
410
         */
411 155
        return $embeddedDocumentValue;
412
    }
413
414
    /**
415
     * Returns the embedded document or reference representation to be stored.
416
     *
417
     * @param array  $mapping
418
     * @param object $document
419
     * @param bool   $includeNestedCollections
420
     * @return array|object|null
421
     * @throws \InvalidArgumentException If the mapping is neither embedded nor reference.
422
     */
423 20
    public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false)
424
    {
425 20
        if (isset($mapping['embedded'])) {
426 7
            return $this->prepareEmbeddedDocumentValue($mapping, $document, $includeNestedCollections);
427
        }
428
429 17
        if (isset($mapping['reference'])) {
430 17
            return $this->prepareReferencedDocumentValue($mapping, $document);
431
        }
432
433
        throw new \InvalidArgumentException('Mapping is neither embedded nor reference.');
434
    }
435
436
    /**
437
     * Returns the collection representation to be stored and unschedules it afterwards.
438
     *
439
     * @param bool $includeNestedCollections
440
     * @return array
441
     */
442 205
    public function prepareAssociatedCollectionValue(PersistentCollectionInterface $coll, $includeNestedCollections = false)
443
    {
444 205
        $mapping = $coll->getMapping();
445 205
        $pb = $this;
446 205
        $callback = isset($mapping['embedded'])
447
            ? function ($v) use ($pb, $mapping, $includeNestedCollections) {
448 100
                return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections);
449 103
            }
450
            : function ($v) use ($pb, $mapping) {
451 115
                return $pb->prepareReferencedDocumentValue($mapping, $v);
452 205
            };
453
454 205
        $setData = $coll->map($callback)->toArray();
455 205
        if (CollectionHelper::isList($mapping['strategy'])) {
456 191
            $setData = array_values($setData);
457
        }
458
459 205
        $this->uow->unscheduleCollectionDeletion($coll);
460 205
        $this->uow->unscheduleCollectionUpdate($coll);
461
462 205
        return $setData;
463
    }
464
}
465