Completed
Push — master ( bce26f...f3ed76 )
by Andreas
17s
created

ODM/MongoDB/Persisters/PersistenceBuilder.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 1123
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
44
    {
45 1123
        $this->dm = $dm;
46 1123
        $this->uow = $uow;
47 1123
    }
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 515
    public function prepareInsertData($document)
56
    {
57 515
        $class = $this->dm->getClassMetadata(get_class($document));
58 515
        $changeset = $this->uow->getDocumentChangeSet($document);
59
60 515
        $insertData = [];
61 515
        foreach ($class->fieldMappings as $mapping) {
62 515
            $new = $changeset[$mapping['fieldName']][1] ?? null;
63
64 515
            if ($new === null && $mapping['nullable']) {
65 160
                $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 515
            if ($new === null) {
72 367
                continue;
73
            }
74
75
            // @Field, @String, @Date, etc.
76 513
            if (! isset($mapping['association'])) {
77 513
                $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new);
78
79
            // @ReferenceOne
80 410
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
81 100
                $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
82
83
            // @EmbedOne
84 386
            } 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 368
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
91 368
                    && $mapping['strategy'] !== ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET && ! $new->isEmpty()) {
92 513
                $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
93
            }
94
        }
95
96
        // add discriminator if the class has one
97 514
        if (isset($class->discriminatorField)) {
98 32
            $insertData[$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
99
        }
100
101 514
        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 220
    public function prepareUpdateData($document)
111
    {
112 220
        $class = $this->dm->getClassMetadata(get_class($document));
113 220
        $changeset = $this->uow->getDocumentChangeSet($document);
114
115 220
        $updateData = [];
116 220
        foreach ($changeset as $fieldName => $change) {
117 219
            $mapping = $class->fieldMappings[$fieldName];
118
119
            // skip non embedded document identifiers
120 219
            if (! $class->isEmbeddedDocument && ! empty($mapping['id'])) {
121 2
                continue;
122
            }
123
124 218
            list($old, $new) = $change;
125
126
            // Scalar fields
127 218
            if (! isset($mapping['association'])) {
128 113
                if ($new === null && $mapping['nullable'] !== true) {
129 1
                    $updateData['$unset'][$mapping['name']] = true;
130
                } else {
131 113
                    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 110
                        $operator = '$set';
136 110
                        $value = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
137
                    }
138
139 113
                    $updateData[$operator][$mapping['name']] = $value;
140
                }
141
142
            // @EmbedOne
143 147
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
144
                // If we have a new embedded document then lets set the whole thing
145 29
                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 22
                } elseif (! $new) {
150 3
                    $updateData['$unset'][$mapping['name']] = true;
151
152
                // Update existing embedded document
153 View Code Duplication
                } else {
154 19
                    $update = $this->prepareUpdateData($new);
155 19
                    foreach ($update as $cmd => $values) {
156 15
                        foreach ($values as $key => $value) {
157 29
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
158
                        }
159
                    }
160
                }
161
162
            // @ReferenceMany, @EmbedMany
163 130
            } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) {
164 120
                if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) {
165 20
                    $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
166 102 View Code Duplication
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) {
167 2
                    $updateData['$unset'][$mapping['name']] = true;
168 2
                    $this->uow->unscheduleCollectionDeletion($new);
169 100
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
170 2
                    $updateData['$unset'][$mapping['name']] = true;
171 2
                    $this->uow->unscheduleCollectionDeletion($old);
172 98
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
173 60
                    foreach ($new as $key => $embeddedDoc) {
174 52
                        if ($this->uow->isScheduledForInsert($embeddedDoc)) {
175 36
                            continue;
176
                        }
177
178 40
                        $update = $this->prepareUpdateData($embeddedDoc);
179 40
                        foreach ($update as $cmd => $values) {
180 14
                            foreach ($values as $name => $value) {
181 120
                                $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value;
182
                            }
183
                        }
184
                    }
185
                }
186
187
            // @ReferenceOne
188 16
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
189 12 View Code Duplication
                if (isset($new) || $mapping['nullable'] === true) {
190 12
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
191
                } else {
192 218
                    $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 220
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
199 99
            $mapping = $coll->getMapping();
200 99
            if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) {
201 3
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true);
202 96 View Code Duplication
            } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($coll)) {
203 1
                $updateData['$unset'][$mapping['name']] = true;
204 99
                $this->uow->unscheduleCollectionDeletion($coll);
205
            }
206
            // @ReferenceMany is handled by CollectionPersister
207
        }
208 220
        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 86
    public function prepareUpsertData($document)
218
    {
219 86
        $class = $this->dm->getClassMetadata(get_class($document));
220 86
        $changeset = $this->uow->getDocumentChangeSet($document);
221
222 86
        $updateData = [];
223 86
        foreach ($changeset as $fieldName => $change) {
224 86
            $mapping = $class->fieldMappings[$fieldName];
225
226 86
            list($old, $new) = $change;
227
228
            // Scalar fields
229 86
            if (! isset($mapping['association'])) {
230 86
                if ($new !== null) {
231 86
                    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 86
                        $operator = '$set';
236 86
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($new);
237
                    }
238
239 86
                    $updateData[$operator][$mapping['name']] = $value;
240 11
                } elseif ($mapping['nullable'] === true) {
241 86
                    $updateData['$setOnInsert'][$mapping['name']] = null;
242
                }
243
244
            // @EmbedOne
245 27
            } 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
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 24 View Code Duplication
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
262 13
                if (isset($new) || $mapping['nullable'] === true) {
263 13
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
264
                }
265
266
            // @ReferenceMany, @EmbedMany
267 15
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
268 15
                    && $new instanceof PersistentCollectionInterface && $new->isDirty()
269 15
                    && CollectionHelper::isAtomic($mapping['strategy'])) {
270 86
                $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 86
        if (isset($class->discriminatorField)) {
277 5
            $updateData['$set'][$class->discriminatorField] = $class->discriminatorValue ?? $class->name;
278
        }
279
280 86
        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 212
    public function prepareReferencedDocumentValue(array $referenceMapping, $document)
294
    {
295 212
        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 179
    public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false)
319
    {
320 179
        $embeddedDocumentValue = [];
321 179
        $class = $this->dm->getClassMetadata(get_class($embeddedDocument));
322
323 179
        foreach ($class->fieldMappings as $mapping) {
324
            // Skip notSaved fields
325 177
            if (! empty($mapping['notSaved'])) {
326 1
                continue;
327
            }
328
329
            // Inline ClassMetadata::getFieldValue()
330 177
            $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument);
331
332 177
            $value = null;
333
334 177
            if ($rawValue !== null) {
335 174
                switch ($mapping['association'] ?? null) {
336
                    // @Field, @String, @Date, etc.
337
                    case null:
338 168
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
339 168
                        break;
340
341 65
                    case ClassMetadata::EMBED_ONE:
342 62
                    case ClassMetadata::REFERENCE_ONE:
343
                        // Nested collections should only be included for embedded relationships
344 21
                        $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded']));
345 21
                        break;
346
347 46
                    case ClassMetadata::EMBED_MANY:
348 4
                    case ClassMetadata::REFERENCE_MANY:
349
                        // Skip PersistentCollections already scheduled for deletion
350 46
                        if (! $includeNestedCollections && $rawValue instanceof PersistentCollectionInterface
351 46
                            && $this->uow->isCollectionScheduledForDeletion($rawValue)) {
352
                            break;
353
                        }
354
355 46
                        $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
356 46
                        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 177
            if ($value === null && $mapping['nullable'] === false) {
365 63
                continue;
366
            }
367
368 174
            $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 179
        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 179
        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 179
        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 175
        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 21
    public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false)
424
    {
425 21
        if (isset($mapping['embedded'])) {
426 8
            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 225
    public function prepareAssociatedCollectionValue(PersistentCollectionInterface $coll, $includeNestedCollections = false)
443
    {
444 225
        $mapping = $coll->getMapping();
445 225
        $pb = $this;
446 225
        $callback = isset($mapping['embedded'])
447
            ? function ($v) use ($pb, $mapping, $includeNestedCollections) {
448 119
                return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections);
449 122
            }
450
            : function ($v) use ($pb, $mapping) {
451 116
                return $pb->prepareReferencedDocumentValue($mapping, $v);
452 225
            };
453
454 225
        $setData = $coll->map($callback)->toArray();
455 225
        if (CollectionHelper::isList($mapping['strategy'])) {
456 204
            $setData = array_values($setData);
457
        }
458
459 225
        $this->uow->unscheduleCollectionDeletion($coll);
460 225
        $this->uow->unscheduleCollectionUpdate($coll);
461
462 225
        return $setData;
463
    }
464
}
465