Completed
Pull Request — master (#1709)
by Andreas
16:45 queued 14:38
created

prepareAssociatedDocumentValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

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