Completed
Pull Request — master (#2128)
by Maciej
20:42 queued 08:45
created

PersistenceBuilder   F

Complexity

Total Complexity 117

Size/Duplication

Total Lines 481
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 94.95%

Importance

Changes 0
Metric Value
wmc 117
lcom 1
cbo 6
dl 0
loc 481
ccs 188
cts 198
cp 0.9495
rs 2
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
C prepareInsertData() 0 58 17
F prepareUpdateData() 0 103 42
D prepareUpsertData() 0 77 28
A prepareReferencedDocumentValue() 0 4 1
F prepareEmbeddedDocumentValue() 0 101 22
A prepareAssociatedDocumentValue() 0 12 3
A prepareAssociatedCollectionValue() 0 22 3

How to fix   Complexity   

Complex Class

Complex classes like PersistenceBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PersistenceBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
9
use Doctrine\ODM\MongoDB\Mapping\MappingException;
10
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
11
use Doctrine\ODM\MongoDB\Types\Incrementable;
12
use Doctrine\ODM\MongoDB\Types\Type;
13
use Doctrine\ODM\MongoDB\UnitOfWork;
14
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
15
use InvalidArgumentException;
16
use UnexpectedValueException;
17
use function array_search;
18
use function array_values;
19
use function get_class;
20
21
/**
22
 * PersistenceBuilder builds the queries used by the persisters to update and insert
23
 * documents when a DocumentManager is flushed. It uses the changeset information in the
24
 * UnitOfWork to build queries using atomic operators like $set, $unset, etc.
25
 *
26
 * @internal
27
 */
28
final class PersistenceBuilder
29
{
30
    /**
31
     * The DocumentManager instance.
32
     *
33
     * @var DocumentManager
34
     */
35
    private $dm;
36
37
    /**
38
     * The UnitOfWork instance.
39
     *
40
     * @var UnitOfWork
41
     */
42
    private $uow;
43
44
    /**
45
     * Initializes a new PersistenceBuilder instance.
46
     */
47 1223
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
48
    {
49 1223
        $this->dm  = $dm;
50 1223
        $this->uow = $uow;
51 1223
    }
52
53
    /**
54
     * Prepares the array that is ready to be inserted to mongodb for a given object document.
55
     *
56
     * @param object $document
57
     *
58
     * @return array $insertData
59
     */
60 548
    public function prepareInsertData($document)
61
    {
62 548
        $class     = $this->dm->getClassMetadata(get_class($document));
63 548
        $changeset = $this->uow->getDocumentChangeSet($document);
64
65 548
        $insertData = [];
66 548
        foreach ($class->fieldMappings as $mapping) {
67 548
            $new = $changeset[$mapping['fieldName']][1] ?? null;
68
69 548
            if ($new === null && $mapping['nullable']) {
70 162
                $insertData[$mapping['name']] = null;
71
            }
72
73
            /* Nothing more to do for null values, since we're either storing
74
             * them (if nullable was true) or not.
75
             */
76 548
            if ($new === null) {
77 373
                continue;
78
            }
79
80
            // @Field, @String, @Date, etc.
81 548
            if (! isset($mapping['association'])) {
82 548
                $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new);
83
84
            // @ReferenceOne
85 432
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
86 109
                $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
87
88
            // @EmbedOne
89 399
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
90 60
                $insertData[$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
91
92
            // @ReferenceMany, @EmbedMany
93
            // We're excluding collections using addToSet since there is a risk
94
            // of duplicated entries stored in the collection
95 381
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
96 381
                    && $mapping['strategy'] !== ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET && ! $new->isEmpty()) {
97 216
                $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
98
            }
99
        }
100
101
        // add discriminator if the class has one
102 535
        if (isset($class->discriminatorField)) {
103 30
            $discriminatorValue = $class->discriminatorValue;
104
105 30
            if ($discriminatorValue === null) {
106 7
                if (! empty($class->discriminatorMap)) {
107 4
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
108
                }
109
110 3
                $discriminatorValue = $class->name;
111
            }
112
113 29
            $insertData[$class->discriminatorField] = $discriminatorValue;
114
        }
115
116 535
        return $insertData;
117
    }
118
119
    /**
120
     * Prepares the update query to update a given document object in mongodb.
121
     *
122
     * @param object $document
123
     *
124
     * @return array $updateData
125
     */
126 241
    public function prepareUpdateData($document)
127
    {
128 241
        $class     = $this->dm->getClassMetadata(get_class($document));
129 241
        $changeset = $this->uow->getDocumentChangeSet($document);
130
131 241
        $updateData = [];
132 241
        foreach ($changeset as $fieldName => $change) {
133 240
            $mapping = $class->fieldMappings[$fieldName];
134
135
            // skip non embedded document identifiers
136 240
            if (! $class->isEmbeddedDocument && ! empty($mapping['id'])) {
137 2
                continue;
138
            }
139
140 239
            [$old, $new] = $change;
0 ignored issues
show
Bug introduced by
The variable $old does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $new does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
141
142
            // Scalar fields
143 239
            if (! isset($mapping['association'])) {
144 128
                if ($new === null && $mapping['nullable'] !== true) {
145 2
                    $updateData['$unset'][$mapping['name']] = true;
146
                } else {
147 127
                    if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
148 4
                        $operator = '$inc';
149 4
                        $type = Type::getType($mapping['type']);
150 4
                        assert($type instanceof Incrementable);
151 4
                        $value = $type->convertToDatabaseValue($type->diff($old, $new));
152
                    } else {
153 124
                        $operator = '$set';
154 124
                        $value    = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
155
                    }
156
157 128
                    $updateData[$operator][$mapping['name']] = $value;
158
                }
159
160
            // @EmbedOne
161 153
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
162
                // If we have a new embedded document then lets set the whole thing
163 29
                if ($new && $this->uow->isScheduledForInsert($new)) {
164 10
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
165
166
                // If we don't have a new value then lets unset the embedded document
167 22
                } elseif (! $new) {
168 3
                    $updateData['$unset'][$mapping['name']] = true;
169
170
                // Update existing embedded document
171
                } else {
172 19
                    $update = $this->prepareUpdateData($new);
173 29
                    foreach ($update as $cmd => $values) {
174 15
                        foreach ($values as $key => $value) {
175 15
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
176
                        }
177
                    }
178
                }
179
180
            // @ReferenceMany, @EmbedMany
181 136
            } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) {
182 126
                if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) {
183 20
                    $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
184 108
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) {
185 2
                    $updateData['$unset'][$mapping['name']] = true;
186 2
                    $this->uow->unscheduleCollectionDeletion($new);
187 106
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
188 2
                    $updateData['$unset'][$mapping['name']] = true;
189 2
                    $this->uow->unscheduleCollectionDeletion($old);
190 104
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
191 126
                    foreach ($new as $key => $embeddedDoc) {
192 58
                        if ($this->uow->isScheduledForInsert($embeddedDoc)) {
193 42
                            continue;
194
                        }
195
196 45
                        $update = $this->prepareUpdateData($embeddedDoc);
197 45
                        foreach ($update as $cmd => $values) {
198 14
                            foreach ($values as $name => $value) {
199 14
                                $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value;
200
                            }
201
                        }
202
                    }
203
                }
204
205
            // @ReferenceOne
206 16
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
207 12
                if (isset($new) || $mapping['nullable'] === true) {
208 12
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
0 ignored issues
show
Documentation introduced by
$new is of type null, but the function expects a object.

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...
209
                } else {
210 2
                    $updateData['$unset'][$mapping['name']] = true;
211
                }
212
            }
213
        }
214
        // collections that aren't dirty but could be subject to update are
215
        // excluded from change set, let's go through them now
216 241
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
217 105
            $mapping = $coll->getMapping();
218 105
            if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) {
219 3
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true);
220 102
            } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($coll)) {
221 1
                $updateData['$unset'][$mapping['name']] = true;
222 1
                $this->uow->unscheduleCollectionDeletion($coll);
223
            }
224
            // @ReferenceMany is handled by CollectionPersister
225
        }
226
227 241
        return $updateData;
228
    }
229
230
    /**
231
     * Prepares the update query to upsert a given document object in mongodb.
232
     *
233
     * @param object $document
234
     *
235
     * @return array $updateData
236
     */
237 88
    public function prepareUpsertData($document)
238
    {
239 88
        $class     = $this->dm->getClassMetadata(get_class($document));
240 88
        $changeset = $this->uow->getDocumentChangeSet($document);
241
242 88
        $updateData = [];
243 88
        foreach ($changeset as $fieldName => $change) {
244 88
            $mapping = $class->fieldMappings[$fieldName];
245
246 88
            [$old, $new] = $change;
0 ignored issues
show
Bug introduced by
The variable $old does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $new does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
247
248
            // Scalar fields
249 88
            if (! isset($mapping['association'])) {
250 88
                if ($new !== null) {
251 88
                    if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
252 3
                        $operator = '$inc';
253 3
                        $type = Type::getType($mapping['type']);
254 3
                        assert($type instanceof Incrementable);
255 3
                        $value = $type->convertToDatabaseValue($type->diff($old, $new));
256
                    } else {
257 88
                        $operator = '$set';
258 88
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new);
259
                    }
260
261 88
                    $updateData[$operator][$mapping['name']] = $value;
262 11
                } elseif ($mapping['nullable'] === true) {
263 88
                    $updateData['$setOnInsert'][$mapping['name']] = null;
264
                }
265
266
            // @EmbedOne
267 29
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
268
                // If we don't have a new value then do nothing on upsert
269
                // If we have a new embedded document then lets set the whole thing
270 8
                if ($new && $this->uow->isScheduledForInsert($new)) {
271 5
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
272 3
                } elseif ($new) {
273
                    // Update existing embedded document
274
                    $update = $this->prepareUpsertData($new);
275 8
                    foreach ($update as $cmd => $values) {
276
                        foreach ($values as $key => $value) {
277
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
278
                        }
279
                    }
280
                }
281
282
            // @ReferenceOne
283 26
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
284 13
                if (isset($new) || $mapping['nullable'] === true) {
285 13
                    $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new);
0 ignored issues
show
Documentation introduced by
$new is of type null, but the function expects a object.

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...
286
                }
287
288
            // @ReferenceMany, @EmbedMany
289 17
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
290 17
                    && $new instanceof PersistentCollectionInterface && $new->isDirty()
291 17
                    && CollectionHelper::isAtomic($mapping['strategy'])) {
292 1
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
293
            }
294
            // @EmbedMany and @ReferenceMany are handled by CollectionPersister
295
        }
296
297
        // add discriminator if the class has one
298 88
        if (isset($class->discriminatorField)) {
299 5
            $discriminatorValue = $class->discriminatorValue;
300
301 5
            if ($discriminatorValue === null) {
302
                if (! empty($class->discriminatorMap)) {
303
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
304
                }
305
306
                $discriminatorValue = $class->name;
307
            }
308
309 5
            $updateData['$set'][$class->discriminatorField] = $discriminatorValue;
310
        }
311
312 88
        return $updateData;
313
    }
314
315
    /**
316
     * Returns the reference representation to be stored in MongoDB.
317
     *
318
     * If the document does not have an identifier and the mapping calls for a
319
     * simple reference, null may be returned.
320
     *
321
     * @param array  $referenceMapping
322
     * @param object $document
323
     *
324
     * @return array|null
325
     */
326 223
    public function prepareReferencedDocumentValue(array $referenceMapping, $document)
327
    {
328 223
        return $this->dm->createReference($document, $referenceMapping);
329
    }
330
331
    /**
332
     * Returns the embedded document to be stored in MongoDB.
333
     *
334
     * The return value will usually be an associative array with string keys
335
     * corresponding to field names on the embedded document. An object may be
336
     * returned if the document is empty, to ensure that a BSON object will be
337
     * stored in lieu of an array.
338
     *
339
     * If $includeNestedCollections is true, nested collections will be included
340
     * in this prepared value and the option will cascade to all embedded
341
     * associations. If any nested PersistentCollections (embed or reference)
342
     * within this value were previously scheduled for deletion or update, they
343
     * will also be unscheduled.
344
     *
345
     * @param array  $embeddedMapping
346
     * @param object $embeddedDocument
347
     * @param bool   $includeNestedCollections
348
     *
349
     * @return array|object
350
     *
351
     * @throws UnexpectedValueException If an unsupported associating mapping is found.
352
     */
353 189
    public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false)
354
    {
355 189
        $embeddedDocumentValue = [];
356 189
        $class                 = $this->dm->getClassMetadata(get_class($embeddedDocument));
357
358 189
        foreach ($class->fieldMappings as $mapping) {
359
            // Skip notSaved fields
360 187
            if (! empty($mapping['notSaved'])) {
361 1
                continue;
362
            }
363
364
            // Inline ClassMetadata::getFieldValue()
365 187
            $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument);
366
367 187
            $value = null;
368
369 187
            if ($rawValue !== null) {
370 184
                switch ($mapping['association'] ?? null) {
371
                    // @Field, @String, @Date, etc.
372
                    case null:
373 176
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
374 176
                        break;
375
376 75
                    case ClassMetadata::EMBED_ONE:
377 70
                    case ClassMetadata::REFERENCE_ONE:
378
                        // Nested collections should only be included for embedded relationships
379 24
                        $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded']));
380 24
                        break;
381
382 53
                    case ClassMetadata::EMBED_MANY:
383 4
                    case ClassMetadata::REFERENCE_MANY:
384
                        // Skip PersistentCollections already scheduled for deletion
385 53
                        if (! $includeNestedCollections && $rawValue instanceof PersistentCollectionInterface
386 53
                            && $this->uow->isCollectionScheduledForDeletion($rawValue)) {
387
                            break;
388
                        }
389
390 53
                        $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
391 53
                        break;
392
393
                    default:
394
                        throw new UnexpectedValueException('Unsupported mapping association: ' . $mapping['association']);
395
                }
396
            }
397
398
            // Omit non-nullable fields that would have a null value
399 187
            if ($value === null && $mapping['nullable'] === false) {
400 65
                continue;
401
            }
402
403 184
            $embeddedDocumentValue[$mapping['name']] = $value;
404
        }
405
406
        /* Add a discriminator value if the embedded document is not mapped
407
         * explicitly to a targetDocument class.
408
         */
409 189
        if (! isset($embeddedMapping['targetDocument'])) {
410 16
            $discriminatorField = $embeddedMapping['discriminatorField'];
411 16
            if (! empty($embeddedMapping['discriminatorMap'])) {
412 5
                $discriminatorValue = array_search($class->name, $embeddedMapping['discriminatorMap']);
413
414 5
                if ($discriminatorValue === false) {
415 5
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
416
                }
417
            } else {
418 11
                $discriminatorValue = $class->name;
419
            }
420
421 15
            $embeddedDocumentValue[$discriminatorField] = $discriminatorValue;
422
        }
423
424
        /* If the class has a discriminator (field and value), use it. A child
425
         * class that is not defined in the discriminator map may only have a
426
         * discriminator field and no value, so default to the full class name.
427
         */
428 188
        if (isset($class->discriminatorField)) {
429 8
            $discriminatorValue = $class->discriminatorValue;
430
431 8
            if ($discriminatorValue === null) {
432 4
                if (! empty($class->discriminatorMap)) {
433 4
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
434
                }
435
436
                $discriminatorValue = $class->name;
437
            }
438
439 6
            $embeddedDocumentValue[$class->discriminatorField] = $discriminatorValue;
440
        }
441
442
        // Ensure empty embedded documents are stored as BSON objects
443 186
        if (empty($embeddedDocumentValue)) {
444 8
            return (object) $embeddedDocumentValue;
445
        }
446
447
        /* @todo Consider always casting the return value to an object, or
448
         * building $embeddedDocumentValue as an object instead of an array, to
449
         * handle the edge case where all database field names are sequential,
450
         * numeric keys.
451
         */
452 182
        return $embeddedDocumentValue;
453
    }
454
455
    /**
456
     * Returns the embedded document or reference representation to be stored.
457
     *
458
     * @param array  $mapping
459
     * @param object $document
460
     * @param bool   $includeNestedCollections
461
     *
462
     * @return array|object|null
463
     *
464
     * @throws InvalidArgumentException If the mapping is neither embedded nor reference.
465
     */
466 24
    public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false)
467
    {
468 24
        if (isset($mapping['embedded'])) {
469 11
            return $this->prepareEmbeddedDocumentValue($mapping, $document, $includeNestedCollections);
470
        }
471
472 18
        if (isset($mapping['reference'])) {
473 18
            return $this->prepareReferencedDocumentValue($mapping, $document);
474
        }
475
476
        throw new InvalidArgumentException('Mapping is neither embedded nor reference.');
477
    }
478
479
    /**
480
     * Returns the collection representation to be stored and unschedules it afterwards.
481
     *
482
     * @param bool $includeNestedCollections
483
     *
484
     * @return array
485
     */
486 232
    public function prepareAssociatedCollectionValue(PersistentCollectionInterface $coll, $includeNestedCollections = false)
487
    {
488 232
        $mapping  = $coll->getMapping();
489 232
        $pb       = $this;
490 232
        $callback = isset($mapping['embedded'])
491
            ? static function ($v) use ($pb, $mapping, $includeNestedCollections) {
492 125
                return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections);
493 129
            }
494
            : static function ($v) use ($pb, $mapping) {
495 117
                return $pb->prepareReferencedDocumentValue($mapping, $v);
496 232
            };
497
498 232
        $setData = $coll->map($callback)->toArray();
499 227
        if (CollectionHelper::isList($mapping['strategy'])) {
500 206
            $setData = array_values($setData);
501
        }
502
503 227
        $this->uow->unscheduleCollectionDeletion($coll);
504 227
        $this->uow->unscheduleCollectionUpdate($coll);
505
506 227
        return $setData;
507
    }
508
}
509