Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

ODM/MongoDB/Persisters/PersistenceBuilder.php (3 issues)

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
174 20
                    $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
175 108
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) {
0 ignored issues
show
$new is of type integer|double, but the function expects a object<Doctrine\ODM\Mong...entCollectionInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
176 2
                    $updateData['$unset'][$mapping['name']] = true;
177 2
                    $this->uow->unscheduleCollectionDeletion($new);
0 ignored issues
show
$new is of type integer|double, but the function expects a object<Doctrine\ODM\Mong...entCollectionInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
178 106
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
179 2
                    $updateData['$unset'][$mapping['name']] = true;
180 2
                    $this->uow->unscheduleCollectionDeletion($old);
181 104
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
182 126
                    foreach ($new as $key => $embeddedDoc) {
183 58
                        if ($this->uow->isScheduledForInsert($embeddedDoc)) {
184 42
                            continue;
185
                        }
186
187 45
                        $update = $this->prepareUpdateData($embeddedDoc);
188 45
                        foreach ($update as $cmd => $values) {
189 14
                            foreach ($values as $name => $value) {
190 14
                                $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value;
191
                            }
192
                        }
193
                    }
194
                }
195
196
            // @ReferenceOne
197 16
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
198 12
                if (isset($new) || $mapping['nullable'] === true) {
199 12
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
200
                } else {
201 2
                    $updateData['$unset'][$mapping['name']] = true;
202
                }
203
            }
204
        }
205
        // collections that aren't dirty but could be subject to update are
206
        // excluded from change set, let's go through them now
207 228
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
208 105
            $mapping = $coll->getMapping();
209 105
            if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) {
210 3
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true);
211 102
            } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($coll)) {
212 1
                $updateData['$unset'][$mapping['name']] = true;
213 1
                $this->uow->unscheduleCollectionDeletion($coll);
214
            }
215
            // @ReferenceMany is handled by CollectionPersister
216
        }
217 228
        return $updateData;
218
    }
219
220
    /**
221
     * Prepares the update query to upsert a given document object in mongodb.
222
     *
223
     * @param object $document
224
     *
225
     * @return array $updateData
226
     */
227 86
    public function prepareUpsertData($document)
228
    {
229 86
        $class     = $this->dm->getClassMetadata(get_class($document));
230 86
        $changeset = $this->uow->getDocumentChangeSet($document);
231
232 86
        $updateData = [];
233 86
        foreach ($changeset as $fieldName => $change) {
234 86
            $mapping = $class->fieldMappings[$fieldName];
235
236 86
            [$old, $new] = $change;
237
238
            // Scalar fields
239 86
            if (! isset($mapping['association'])) {
240 86
                if ($new !== null) {
241 86
                    if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
242 3
                        $operator = '$inc';
243 3
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
244
                    } else {
245 86
                        $operator = '$set';
246 86
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new);
247
                    }
248
249 86
                    $updateData[$operator][$mapping['name']] = $value;
250 11
                } elseif ($mapping['nullable'] === true) {
251 86
                    $updateData['$setOnInsert'][$mapping['name']] = null;
252
                }
253
254
            // @EmbedOne
255 27
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
256
                // If we don't have a new value then do nothing on upsert
257
                // If we have a new embedded document then lets set the whole thing
258 8
                if ($new && $this->uow->isScheduledForInsert($new)) {
259 5
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
260 3
                } elseif ($new) {
261
                    // Update existing embedded document
262
                    $update = $this->prepareUpsertData($new);
263 8
                    foreach ($update as $cmd => $values) {
264
                        foreach ($values as $key => $value) {
265
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
266
                        }
267
                    }
268
                }
269
270
            // @ReferenceOne
271 24
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
272 13
                if (isset($new) || $mapping['nullable'] === true) {
273 13
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
274
                }
275
276
            // @ReferenceMany, @EmbedMany
277 15
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
278 15
                    && $new instanceof PersistentCollectionInterface && $new->isDirty()
279 15
                    && CollectionHelper::isAtomic($mapping['strategy'])) {
280 1
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
281
            }
282
            // @EmbedMany and @ReferenceMany are handled by CollectionPersister
283
        }
284
285
        // add discriminator if the class has one
286 86
        if (isset($class->discriminatorField)) {
287 5
            if ($class->discriminatorValue === null) {
288
                if (! empty($class->discriminatorMap)) {
289
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
290
                }
291
                $class->discriminatorValue = $class->name;
292
            }
293 5
            $updateData['$set'][$class->discriminatorField] = $class->discriminatorValue;
294
        }
295
296 86
        return $updateData;
297
    }
298
299
    /**
300
     * Returns the reference representation to be stored in MongoDB.
301
     *
302
     * If the document does not have an identifier and the mapping calls for a
303
     * simple reference, null may be returned.
304
     *
305
     * @param array  $referenceMapping
306
     * @param object $document
307
     *
308
     * @return array|null
309
     */
310 212
    public function prepareReferencedDocumentValue(array $referenceMapping, $document)
311
    {
312 212
        return $this->dm->createReference($document, $referenceMapping);
313
    }
314
315
    /**
316
     * Returns the embedded document to be stored in MongoDB.
317
     *
318
     * The return value will usually be an associative array with string keys
319
     * corresponding to field names on the embedded document. An object may be
320
     * returned if the document is empty, to ensure that a BSON object will be
321
     * stored in lieu of an array.
322
     *
323
     * If $includeNestedCollections is true, nested collections will be included
324
     * in this prepared value and the option will cascade to all embedded
325
     * associations. If any nested PersistentCollections (embed or reference)
326
     * within this value were previously scheduled for deletion or update, they
327
     * will also be unscheduled.
328
     *
329
     * @param array  $embeddedMapping
330
     * @param object $embeddedDocument
331
     * @param bool   $includeNestedCollections
332
     *
333
     * @return array|object
334
     *
335
     * @throws UnexpectedValueException If an unsupported associating mapping is found.
336
     */
337 186
    public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false)
338
    {
339 186
        $embeddedDocumentValue = [];
340 186
        $class                 = $this->dm->getClassMetadata(get_class($embeddedDocument));
341
342 186
        foreach ($class->fieldMappings as $mapping) {
343
            // Skip notSaved fields
344 184
            if (! empty($mapping['notSaved'])) {
345 1
                continue;
346
            }
347
348
            // Inline ClassMetadata::getFieldValue()
349 184
            $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument);
350
351 184
            $value = null;
352
353 184
            if ($rawValue !== null) {
354 181
                switch ($mapping['association'] ?? null) {
355
                    // @Field, @String, @Date, etc.
356
                    case null:
357 175
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
358 175
                        break;
359
360 72
                    case ClassMetadata::EMBED_ONE:
361 69
                    case ClassMetadata::REFERENCE_ONE:
362
                        // Nested collections should only be included for embedded relationships
363 21
                        $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded']));
364 21
                        break;
365
366 53
                    case ClassMetadata::EMBED_MANY:
367 4
                    case ClassMetadata::REFERENCE_MANY:
368
                        // Skip PersistentCollections already scheduled for deletion
369 53
                        if (! $includeNestedCollections && $rawValue instanceof PersistentCollectionInterface
370 53
                            && $this->uow->isCollectionScheduledForDeletion($rawValue)) {
371
                            break;
372
                        }
373
374 53
                        $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
375 53
                        break;
376
377
                    default:
378
                        throw new UnexpectedValueException('Unsupported mapping association: ' . $mapping['association']);
379
                }
380
            }
381
382
            // Omit non-nullable fields that would have a null value
383 184
            if ($value === null && $mapping['nullable'] === false) {
384 63
                continue;
385
            }
386
387 181
            $embeddedDocumentValue[$mapping['name']] = $value;
388
        }
389
390
        /* Add a discriminator value if the embedded document is not mapped
391
         * explicitly to a targetDocument class.
392
         */
393 186
        if (! isset($embeddedMapping['targetDocument'])) {
394 16
            $discriminatorField = $embeddedMapping['discriminatorField'];
395 16
            if (! empty($embeddedMapping['discriminatorMap'])) {
396 5
                $discriminatorValue = array_search($class->name, $embeddedMapping['discriminatorMap']);
397
398 5
                if ($discriminatorValue === false) {
399 5
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
400
                }
401
            } else {
402 11
                $discriminatorValue = $class->name;
403
            }
404
405 15
            $embeddedDocumentValue[$discriminatorField] = $discriminatorValue;
406
        }
407
408
        /* If the class has a discriminator (field and value), use it. A child
409
         * class that is not defined in the discriminator map may only have a
410
         * discriminator field and no value, so default to the full class name.
411
         */
412 185
        if (isset($class->discriminatorField)) {
413 8
            if ($class->discriminatorValue === null) {
414 4
                if (! empty($class->discriminatorMap)) {
415 4
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
416
                }
417
                $class->discriminatorValue = $class->name;
418
            }
419 6
            $embeddedDocumentValue[$class->discriminatorField] =  $class->discriminatorValue;
420
        }
421
422
        // Ensure empty embedded documents are stored as BSON objects
423 183
        if (empty($embeddedDocumentValue)) {
424 6
            return (object) $embeddedDocumentValue;
425
        }
426
427
        /* @todo Consider always casting the return value to an object, or
428
         * building $embeddedDocumentValue as an object instead of an array, to
429
         * handle the edge case where all database field names are sequential,
430
         * numeric keys.
431
         */
432 179
        return $embeddedDocumentValue;
433
    }
434
435
    /**
436
     * Returns the embedded document or reference representation to be stored.
437
     *
438
     * @param array  $mapping
439
     * @param object $document
440
     * @param bool   $includeNestedCollections
441
     *
442
     * @return array|object|null
443
     *
444
     * @throws InvalidArgumentException If the mapping is neither embedded nor reference.
445
     */
446 21
    public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false)
447
    {
448 21
        if (isset($mapping['embedded'])) {
449 8
            return $this->prepareEmbeddedDocumentValue($mapping, $document, $includeNestedCollections);
450
        }
451
452 17
        if (isset($mapping['reference'])) {
453 17
            return $this->prepareReferencedDocumentValue($mapping, $document);
454
        }
455
456
        throw new InvalidArgumentException('Mapping is neither embedded nor reference.');
457
    }
458
459
    /**
460
     * Returns the collection representation to be stored and unschedules it afterwards.
461
     *
462
     * @param bool $includeNestedCollections
463
     *
464
     * @return array
465
     */
466 232
    public function prepareAssociatedCollectionValue(PersistentCollectionInterface $coll, $includeNestedCollections = false)
467
    {
468 232
        $mapping  = $coll->getMapping();
469 232
        $pb       = $this;
470 232
        $callback = isset($mapping['embedded'])
471
            ? static function ($v) use ($pb, $mapping, $includeNestedCollections) {
472 125
                return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections);
473 129
            }
474
            : static function ($v) use ($pb, $mapping) {
475 117
                return $pb->prepareReferencedDocumentValue($mapping, $v);
476 232
            };
477
478 232
        $setData = $coll->map($callback)->toArray();
479 227
        if (CollectionHelper::isList($mapping['strategy'])) {
480 206
            $setData = array_values($setData);
481
        }
482
483 227
        $this->uow->unscheduleCollectionDeletion($coll);
484 227
        $this->uow->unscheduleCollectionUpdate($coll);
485
486 227
        return $setData;
487
    }
488
}
489