Completed
Push — master ( ce8c34...f078de )
by Andreas
49:52 queued 47:00
created

PersistenceBuilder::prepareEmbeddedDocumentValue()   F

Complexity

Conditions 20
Paths 361

Size

Total Lines 95
Code Lines 42

Duplication

Lines 18
Ratio 18.95 %

Code Coverage

Tests 38
CRAP Score 20.05

Importance

Changes 0
Metric Value
dl 18
loc 95
ccs 38
cts 40
cp 0.95
rs 3.6338
c 0
b 0
f 0
cc 20
eloc 42
nc 361
nop 3
crap 20.05

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Doctrine\ODM\MongoDB\Persisters;
3
4
use Doctrine\ODM\MongoDB\DocumentManager;
5
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
6
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
7
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
8
use Doctrine\ODM\MongoDB\Types\Type;
9
use Doctrine\ODM\MongoDB\UnitOfWork;
10
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
11
12
/**
13
 * PersistenceBuilder builds the queries used by the persisters to update and insert
14
 * documents when a DocumentManager is flushed. It uses the changeset information in the
15
 * UnitOfWork to build queries using atomic operators like $set, $unset, etc.
16
 *
17
 * @since       1.0
18
 */
19
class PersistenceBuilder
20
{
21
    /**
22
     * The DocumentManager instance.
23
     *
24
     * @var DocumentManager
25
     */
26
    private $dm;
27
28
    /**
29
     * The UnitOfWork instance.
30
     *
31
     * @var UnitOfWork
32
     */
33
    private $uow;
34
35
    /**
36
     * Initializes a new PersistenceBuilder instance.
37
     *
38
     * @param DocumentManager $dm
39
     * @param UnitOfWork $uow
40
     */
41 1086
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
42
    {
43 1086
        $this->dm = $dm;
44 1086
        $this->uow = $uow;
45 1086
    }
46
47
    /**
48
     * Prepares the array that is ready to be inserted to mongodb for a given object document.
49
     *
50
     * @param object $document
51
     * @return array $insertData
52
     */
53 487
    public function prepareInsertData($document)
54
    {
55 487
        $class = $this->dm->getClassMetadata(get_class($document));
56 487
        $changeset = $this->uow->getDocumentChangeSet($document);
57
58 487
        $insertData = array();
59 487
        foreach ($class->fieldMappings as $mapping) {
60
61 487
            $new = $changeset[$mapping['fieldName']][1] ?? null;
62
63 487
            if ($new === null && $mapping['nullable']) {
64 150
                $insertData[$mapping['name']] = null;
65
            }
66
67
            /* Nothing more to do for null values, since we're either storing
68
             * them (if nullable was true) or not.
69
             */
70 487
            if ($new === null) {
71 339
                continue;
72
            }
73
74
            // @Field, @String, @Date, etc.
75 487
            if ( ! isset($mapping['association'])) {
76 487
                $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new);
77
78
            // @ReferenceOne
79 390
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
80 101
                $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
81
82
            // @EmbedOne
83 365
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
84 60
                $insertData[$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
85
86
            // @ReferenceMany, @EmbedMany
87
            // We're excluding collections using addToSet since there is a risk
88
            // of duplicated entries stored in the collection
89 347
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
90 347
                    && $mapping['strategy'] !== ClassMetadataInfo::STORAGE_STRATEGY_ADD_TO_SET && ! $new->isEmpty()) {
91 487
                $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
92
            }
93
        }
94
95
        // add discriminator if the class has one
96 486
        if (isset($class->discriminatorField)) {
97 32
            $insertData[$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
98
        }
99
100 486
        return $insertData;
101
    }
102
103
    /**
104
     * Prepares the update query to update a given document object in mongodb.
105
     *
106
     * @param object $document
107
     * @return array $updateData
108
     */
109 200
    public function prepareUpdateData($document)
110
    {
111 200
        $class = $this->dm->getClassMetadata(get_class($document));
112 200
        $changeset = $this->uow->getDocumentChangeSet($document);
113
114 200
        $updateData = array();
115 200
        foreach ($changeset as $fieldName => $change) {
116 199
            $mapping = $class->fieldMappings[$fieldName];
117
118
            // skip non embedded document identifiers
119 199
            if ( ! $class->isEmbeddedDocument && ! empty($mapping['id'])) {
120 2
                continue;
121
            }
122
123 198
            list($old, $new) = $change;
124
125
            // Scalar fields
126 198
            if ( ! isset($mapping['association'])) {
127 107
                if ($new === null && $mapping['nullable'] !== true) {
128 1
                    $updateData['$unset'][$mapping['name']] = true;
129
                } else {
130 107 View Code Duplication
                    if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadataInfo::STORAGE_STRATEGY_INCREMENT) {
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...
131 4
                        $operator = '$inc';
132 4
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
133
                    } else {
134 104
                        $operator = '$set';
135 104
                        $value = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
136
                    }
137
138 107
                    $updateData[$operator][$mapping['name']] = $value;
139
                }
140
141
            // @EmbedOne
142 127
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
143
                // If we have a new embedded document then lets set the whole thing
144 28
                if ($new && $this->uow->isScheduledForInsert($new)) {
145 10
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
146
147
                // If we don't have a new value then lets unset the embedded document
148 21
                } elseif ( ! $new) {
149 3
                    $updateData['$unset'][$mapping['name']] = true;
150
151
                // Update existing embedded document
152 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...
153 18
                    $update = $this->prepareUpdateData($new);
154 18
                    foreach ($update as $cmd => $values) {
155 14
                        foreach ($values as $key => $value) {
156 28
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
157
                        }
158
                    }
159
                }
160
161
            // @ReferenceMany, @EmbedMany
162 110
            } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) {
163 101
                if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) {
164 8
                    $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
165 94 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...
166 1
                    $updateData['$unset'][$mapping['name']] = true;
167 1
                    $this->uow->unscheduleCollectionDeletion($new);
168 93
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
169
                    $updateData['$unset'][$mapping['name']] = true;
170
                    $this->uow->unscheduleCollectionDeletion($old);
171 93
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
172 56
                    foreach ($new as $key => $embeddedDoc) {
173 49
                        if ( ! $this->uow->isScheduledForInsert($embeddedDoc)) {
174 38
                            $update = $this->prepareUpdateData($embeddedDoc);
175 38
                            foreach ($update as $cmd => $values) {
176 13
                                foreach ($values as $name => $value) {
177 101
                                    $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value;
178
                                }
179
                            }
180
                        }
181
                    }
182
                }
183
184
            // @ReferenceOne
185 15
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
186 12 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...
187 12
                    $updateData['$set'][$mapping['name']] = (is_null($new) ? null : $this->prepareReferencedDocumentValue($mapping, $new));
188
                } else {
189 198
                    $updateData['$unset'][$mapping['name']] = true;
190
                }
191
            }
192
        }
193
        // collections that aren't dirty but could be subject to update are
194
        // excluded from change set, let's go through them now
195 200
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
196 93
            $mapping = $coll->getMapping();
197 93
            if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) {
198 2
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true);
199 91 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...
200
                $updateData['$unset'][$mapping['name']] = true;
201 93
                $this->uow->unscheduleCollectionDeletion($coll);
202
            }
203
            // @ReferenceMany is handled by CollectionPersister
204
        }
205 200
        return $updateData;
206
    }
207
208
    /**
209
     * Prepares the update query to upsert a given document object in mongodb.
210
     *
211
     * @param object $document
212
     * @return array $updateData
213
     */
214 86
    public function prepareUpsertData($document)
215
    {
216 86
        $class = $this->dm->getClassMetadata(get_class($document));
217 86
        $changeset = $this->uow->getDocumentChangeSet($document);
218
219 86
        $updateData = array();
220 86
        foreach ($changeset as $fieldName => $change) {
221 86
            $mapping = $class->fieldMappings[$fieldName];
222
223 86
            list($old, $new) = $change;
224
225
            // Scalar fields
226 86
            if ( ! isset($mapping['association'])) {
227 86
                if ($new !== null || $mapping['nullable'] === true) {
228 86 View Code Duplication
                    if ($new !== null && empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadataInfo::STORAGE_STRATEGY_INCREMENT) {
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...
229 3
                        $operator = '$inc';
230 3
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
231
                    } else {
232 86
                        $operator = '$set';
233 86
                        $value = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
234
                    }
235
236 86
                    $updateData[$operator][$mapping['name']] = $value;
237
                }
238
239
            // @EmbedOne
240 26
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
241
                // If we don't have a new value then do nothing on upsert
242
                // If we have a new embedded document then lets set the whole thing
243 8
                if ($new && $this->uow->isScheduledForInsert($new)) {
244 5
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
245 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...
246
                    // Update existing embedded document
247
                    $update = $this->prepareUpsertData($new);
248
                    foreach ($update as $cmd => $values) {
249
                        foreach ($values as $key => $value) {
250 8
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
251
                        }
252
                    }
253
                }
254
255
            // @ReferenceOne
256 23 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...
257 13
                if (isset($new) || $mapping['nullable'] === true) {
258 13
                    $updateData['$set'][$mapping['name']] = (is_null($new) ? null : $this->prepareReferencedDocumentValue($mapping, $new));
259
                }
260
261
            // @ReferenceMany, @EmbedMany
262 14
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
263 14
                    && $new instanceof PersistentCollectionInterface && $new->isDirty()
264 14
                    && CollectionHelper::isAtomic($mapping['strategy'])) {
265 86
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
266
            }
267
            // @EmbedMany and @ReferenceMany are handled by CollectionPersister
268
        }
269
270
        // add discriminator if the class has one
271 86
        if (isset($class->discriminatorField)) {
272 5
            $updateData['$set'][$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
273
        }
274
275 86
        return $updateData;
276
    }
277
278
    /**
279
     * Returns the reference representation to be stored in MongoDB.
280
     *
281
     * If the document does not have an identifier and the mapping calls for a
282
     * simple reference, null may be returned.
283
     *
284
     * @param array $referenceMapping
285
     * @param object $document
286
     * @return array|null
287
     */
288 208
    public function prepareReferencedDocumentValue(array $referenceMapping, $document)
289
    {
290 208
        return $this->dm->createReference($document, $referenceMapping);
291
    }
292
293
    /**
294
     * Returns the embedded document to be stored in MongoDB.
295
     *
296
     * The return value will usually be an associative array with string keys
297
     * corresponding to field names on the embedded document. An object may be
298
     * returned if the document is empty, to ensure that a BSON object will be
299
     * stored in lieu of an array.
300
     *
301
     * If $includeNestedCollections is true, nested collections will be included
302
     * in this prepared value and the option will cascade to all embedded
303
     * associations. If any nested PersistentCollections (embed or reference)
304
     * within this value were previously scheduled for deletion or update, they
305
     * will also be unscheduled.
306
     *
307
     * @param array $embeddedMapping
308
     * @param object $embeddedDocument
309
     * @param boolean $includeNestedCollections
310
     * @return array|object
311
     * @throws \UnexpectedValueException if an unsupported associating mapping is found
312
     */
313 158
    public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false)
314
    {
315 158
        $embeddedDocumentValue = array();
316 158
        $class = $this->dm->getClassMetadata(get_class($embeddedDocument));
317
318 158
        foreach ($class->fieldMappings as $mapping) {
319
            // Skip notSaved fields
320 156
            if ( ! empty($mapping['notSaved'])) {
321 1
                continue;
322
            }
323
324
            // Inline ClassMetadataInfo::getFieldValue()
325 156
            $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument);
326
327 156
            $value = null;
328
329 156
            if ($rawValue !== null) {
330 153
                switch ($mapping['association'] ?? null) {
331
                    // @Field, @String, @Date, etc.
332
                    case null:
333 147
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
334 147
                        break;
335
336 54
                    case ClassMetadata::EMBED_ONE:
337 51
                    case ClassMetadata::REFERENCE_ONE:
338
                        // Nested collections should only be included for embedded relationships
339 19
                        $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded']));
340 19
                        break;
341
342 36
                    case ClassMetadata::EMBED_MANY:
343 4
                    case ClassMetadata::REFERENCE_MANY:
344
                        // Skip PersistentCollections already scheduled for deletion
345 36
                        if ( ! $includeNestedCollections && $rawValue instanceof PersistentCollectionInterface
346 36
                            && $this->uow->isCollectionScheduledForDeletion($rawValue)) {
347
                            break;
348
                        }
349
350 36
                        $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
351 36
                        break;
352
353
                    default:
354
                        throw new \UnexpectedValueException('Unsupported mapping association: ' . $mapping['association']);
355
                }
356
            }
357
358
            // Omit non-nullable fields that would have a null value
359 156
            if ($value === null && $mapping['nullable'] === false) {
360 46
                continue;
361
            }
362
363 153
            $embeddedDocumentValue[$mapping['name']] = $value;
364
        }
365
366
        /* Add a discriminator value if the embedded document is not mapped
367
         * explicitly to a targetDocument class.
368
         */
369 158 View Code Duplication
        if ( ! isset($embeddedMapping['targetDocument'])) {
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...
370 16
            $discriminatorField = $embeddedMapping['discriminatorField'];
371 16
            $discriminatorValue = isset($embeddedMapping['discriminatorMap'])
372 5
                ? array_search($class->name, $embeddedMapping['discriminatorMap'])
373 16
                : $class->name;
374
375
            /* If the discriminator value was not found in the map, use the full
376
             * class name. In the future, it may be preferable to throw an
377
             * exception here (perhaps based on some strictness option).
378
             *
379
             * @see DocumentManager::createDBRef()
380
             */
381 16
            if ($discriminatorValue === false) {
382 2
                $discriminatorValue = $class->name;
383
            }
384
385 16
            $embeddedDocumentValue[$discriminatorField] = $discriminatorValue;
386
        }
387
388
        /* If the class has a discriminator (field and value), use it. A child
389
         * class that is not defined in the discriminator map may only have a
390
         * discriminator field and no value, so default to the full class name.
391
         */
392 158
        if (isset($class->discriminatorField)) {
393 8
            $embeddedDocumentValue[$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
394
        }
395
396
        // Ensure empty embedded documents are stored as BSON objects
397 158
        if (empty($embeddedDocumentValue)) {
398 6
            return (object) $embeddedDocumentValue;
399
        }
400
401
        /* @todo Consider always casting the return value to an object, or
402
         * building $embeddedDocumentValue as an object instead of an array, to
403
         * handle the edge case where all database field names are sequential,
404
         * numeric keys.
405
         */
406 154
        return $embeddedDocumentValue;
407
    }
408
409
    /*
410
     * Returns the embedded document or reference representation to be stored.
411
     *
412
     * @param array $mapping
413
     * @param object $document
414
     * @param boolean $includeNestedCollections
415
     * @return array|object|null
416
     * @throws \InvalidArgumentException if the mapping is neither embedded nor reference
417
     */
418 19
    public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false)
419
    {
420 19
        if (isset($mapping['embedded'])) {
421 7
            return $this->prepareEmbeddedDocumentValue($mapping, $document, $includeNestedCollections);
422
        }
423
424 16
        if (isset($mapping['reference'])) {
425 16
            return $this->prepareReferencedDocumentValue($mapping, $document);
426
        }
427
428
        throw new \InvalidArgumentException('Mapping is neither embedded nor reference.');
429
    }
430
431
    /**
432
     * Returns the collection representation to be stored and unschedules it afterwards.
433
     *
434
     * @param PersistentCollectionInterface $coll
435
     * @param bool $includeNestedCollections
436
     * @return array
437
     */
438 202
    public function prepareAssociatedCollectionValue(PersistentCollectionInterface $coll, $includeNestedCollections = false)
439
    {
440 202
        $mapping = $coll->getMapping();
441 202
        $pb = $this;
442 202
        $callback = isset($mapping['embedded'])
443 102
            ? function($v) use ($pb, $mapping, $includeNestedCollections) {
444 99
                return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections);
445 102
            }
446
            : function($v) use ($pb, $mapping) { return $pb->prepareReferencedDocumentValue($mapping, $v); };
447
448 202
        $setData = $coll->map($callback)->toArray();
449 202
        if (CollectionHelper::isList($mapping['strategy'])) {
450 188
            $setData = array_values($setData);
451
        }
452
453 202
        $this->uow->unscheduleCollectionDeletion($coll);
454 202
        $this->uow->unscheduleCollectionUpdate($coll);
455
456 202
        return $setData;
457
    }
458
}
459