Completed
Push — master ( b90edc...95608f )
by Andreas
14:15 queued 11s
created

prepareAssociatedDocumentValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 6
cp 0
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 3
crap 12
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
 * @internal
26
 */
27
final class PersistenceBuilder
28
{
29
    /**
30
     * The DocumentManager instance.
31
     *
32
     * @var DocumentManager
33
     */
34
    private $dm;
35
36
    /**
37
     * The UnitOfWork instance.
38
     *
39
     * @var UnitOfWork
40
     */
41
    private $uow;
42
43
    /**
44
     * Initializes a new PersistenceBuilder instance.
45
     */
46 572
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
47
    {
48 572
        $this->dm  = $dm;
49 572
        $this->uow = $uow;
50 572
    }
51
52
    /**
53
     * Prepares the array that is ready to be inserted to mongodb for a given object document.
54
     *
55
     * @param object $document
56
     *
57
     * @return array $insertData
58
     */
59 32
    public function prepareInsertData($document)
60
    {
61 32
        $class     = $this->dm->getClassMetadata(get_class($document));
62 32
        $changeset = $this->uow->getDocumentChangeSet($document);
63
64 32
        $insertData = [];
65 32
        foreach ($class->fieldMappings as $mapping) {
66 32
            $new = $changeset[$mapping['fieldName']][1] ?? null;
67
68 32
            if ($new === null && $mapping['nullable']) {
69 4
                $insertData[$mapping['name']] = null;
70
            }
71
72
            /* Nothing more to do for null values, since we're either storing
73
             * them (if nullable was true) or not.
74
             */
75 32
            if ($new === null) {
76 19
                continue;
77
            }
78
79
            // @Field, @String, @Date, etc.
80 32
            if (! isset($mapping['association'])) {
81 32
                $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new);
82
83
            // @ReferenceOne
84 29
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
85 11
                $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
86
87
            // @EmbedOne
88 21
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
89 5
                $insertData[$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
90
91
            // @ReferenceMany, @EmbedMany
92
            // We're excluding collections using addToSet since there is a risk
93
            // of duplicated entries stored in the collection
94 21
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
95 21
                    && $mapping['strategy'] !== ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET && ! $new->isEmpty()) {
96 8
                $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
97
            }
98
        }
99
100
        // add discriminator if the class has one
101 19
        if (isset($class->discriminatorField)) {
102 3
            $discriminatorValue = $class->discriminatorValue;
103
104 3
            if ($discriminatorValue === null) {
105 1
                if (! empty($class->discriminatorMap)) {
106
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
107
                }
108
109 1
                $discriminatorValue = $class->name;
110
            }
111
112 3
            $insertData[$class->discriminatorField] = $discriminatorValue;
113
        }
114
115 19
        return $insertData;
116
    }
117
118
    /**
119
     * Prepares the update query to update a given document object in mongodb.
120
     *
121
     * @param object $document
122
     *
123
     * @return array $updateData
124
     */
125
    public function prepareUpdateData($document)
126
    {
127
        $class     = $this->dm->getClassMetadata(get_class($document));
128
        $changeset = $this->uow->getDocumentChangeSet($document);
129
130
        $updateData = [];
131
        foreach ($changeset as $fieldName => $change) {
132
            $mapping = $class->fieldMappings[$fieldName];
133
134
            // skip non embedded document identifiers
135
            if (! $class->isEmbeddedDocument && ! empty($mapping['id'])) {
136
                continue;
137
            }
138
139
            [$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...
140
141
            // Scalar fields
142
            if (! isset($mapping['association'])) {
143
                if ($new === null && $mapping['nullable'] !== true) {
144
                    $updateData['$unset'][$mapping['name']] = true;
145
                } else {
146
                    if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
147
                        $operator = '$inc';
148
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
149
                    } else {
150
                        $operator = '$set';
151
                        $value    = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
152
                    }
153
154
                    $updateData[$operator][$mapping['name']] = $value;
155
                }
156
157
            // @EmbedOne
158
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
159
                // If we have a new embedded document then lets set the whole thing
160
                if ($new && $this->uow->isScheduledForInsert($new)) {
161
                    $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new);
0 ignored issues
show
Documentation introduced by
$new is of type integer|double, 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...
162
163
                // If we don't have a new value then lets unset the embedded document
164
                } elseif (! $new) {
165
                    $updateData['$unset'][$mapping['name']] = true;
166
167
                // Update existing embedded document
168
                } else {
169
                    $update = $this->prepareUpdateData($new);
0 ignored issues
show
Documentation introduced by
$new is of type integer|double, 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...
170
                    foreach ($update as $cmd => $values) {
171
                        foreach ($values as $key => $value) {
172
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
173
                        }
174
                    }
175
                }
176
177
            // @ReferenceMany, @EmbedMany
178
            } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) {
179
                if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) {
180
                    $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
0 ignored issues
show
Documentation introduced by
$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...
181
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) {
182
                    $updateData['$unset'][$mapping['name']] = true;
183
                    $this->uow->unscheduleCollectionDeletion($new);
184
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
185
                    $updateData['$unset'][$mapping['name']] = true;
186
                    $this->uow->unscheduleCollectionDeletion($old);
187
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
188
                    foreach ($new as $key => $embeddedDoc) {
0 ignored issues
show
Bug introduced by
The expression $new of type integer|double is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

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