Completed
Push — master ( 95608f...d78a07 )
by Andreas
14s queued 10s
created

prepareAssociatedDocumentValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 3
crap 3.0416
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 1212
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
47
    {
48 1212
        $this->dm  = $dm;
49 1212
        $this->uow = $uow;
50 1212
    }
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 543
    public function prepareInsertData($document)
60
    {
61 543
        $class     = $this->dm->getClassMetadata(get_class($document));
62 543
        $changeset = $this->uow->getDocumentChangeSet($document);
63
64 543
        $insertData = [];
65 543
        foreach ($class->fieldMappings as $mapping) {
66 543
            $new = $changeset[$mapping['fieldName']][1] ?? null;
67
68 543
            if ($new === null && $mapping['nullable']) {
69 162
                $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 543
            if ($new === null) {
76 368
                continue;
77
            }
78
79
            // @Field, @String, @Date, etc.
80 543
            if (! isset($mapping['association'])) {
81 543
                $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new);
82
83
            // @ReferenceOne
84 427
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
85 108
                $insertData[$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
86
87
            // @EmbedOne
88 394
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
89 60
                $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 376
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
95 376
                    && $mapping['strategy'] !== ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET && ! $new->isEmpty()) {
96 216
                $insertData[$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true);
97
            }
98
        }
99
100
        // add discriminator if the class has one
101 530
        if (isset($class->discriminatorField)) {
102 30
            $discriminatorValue = $class->discriminatorValue;
103
104 30
            if ($discriminatorValue === null) {
105 7
                if (! empty($class->discriminatorMap)) {
106 4
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
107
                }
108
109 3
                $discriminatorValue = $class->name;
110
            }
111
112 29
            $insertData[$class->discriminatorField] = $discriminatorValue;
113
        }
114
115 530
        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 235
    public function prepareUpdateData($document)
126
    {
127 235
        $class     = $this->dm->getClassMetadata(get_class($document));
128 235
        $changeset = $this->uow->getDocumentChangeSet($document);
129
130 235
        $updateData = [];
131 235
        foreach ($changeset as $fieldName => $change) {
132 234
            $mapping = $class->fieldMappings[$fieldName];
133
134
            // skip non embedded document identifiers
135 234
            if (! $class->isEmbeddedDocument && ! empty($mapping['id'])) {
136 2
                continue;
137
            }
138
139 233
            [$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 233
            if (! isset($mapping['association'])) {
143 122
                if ($new === null && $mapping['nullable'] !== true) {
144 2
                    $updateData['$unset'][$mapping['name']] = true;
145
                } else {
146 121
                    if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
147 4
                        $operator = '$inc';
148 4
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
149
                    } else {
150 118
                        $operator = '$set';
151 118
                        $value    = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new);
152
                    }
153
154 122
                    $updateData[$operator][$mapping['name']] = $value;
155
                }
156
157
            // @EmbedOne
158 153
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) {
159
                // If we have a new embedded document then lets set the whole thing
160 29
                if ($new && $this->uow->isScheduledForInsert($new)) {
161 10
                    $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 22
                } elseif (! $new) {
165 3
                    $updateData['$unset'][$mapping['name']] = true;
166
167
                // Update existing embedded document
168
                } else {
169 19
                    $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 29
                    foreach ($update as $cmd => $values) {
171 15
                        foreach ($values as $key => $value) {
172 15
                            $updateData[$cmd][$mapping['name'] . '.' . $key] = $value;
173
                        }
174
                    }
175
                }
176
177
            // @ReferenceMany, @EmbedMany
178 136
            } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) {
179 126
                if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) {
180 20
                    $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 108
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) {
182 2
                    $updateData['$unset'][$mapping['name']] = true;
183 2
                    $this->uow->unscheduleCollectionDeletion($new);
184 106
                } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($old)) {
185 2
                    $updateData['$unset'][$mapping['name']] = true;
186 2
                    $this->uow->unscheduleCollectionDeletion($old);
187 104
                } elseif ($mapping['association'] === ClassMetadata::EMBED_MANY) {
188 126
                    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 58
                        if ($this->uow->isScheduledForInsert($embeddedDoc)) {
190 42
                            continue;
191
                        }
192
193 45
                        $update = $this->prepareUpdateData($embeddedDoc);
194 45
                        foreach ($update as $cmd => $values) {
195 14
                            foreach ($values as $name => $value) {
196 14
                                $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value;
197
                            }
198
                        }
199
                    }
200
                }
201
202
            // @ReferenceOne
203 16
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
204 12
                if (isset($new) || $mapping['nullable'] === true) {
205 12
                    $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 2
                    $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 235
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
214 105
            $mapping = $coll->getMapping();
215 105
            if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($coll)) {
216 3
                $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($coll, true);
217 102
            } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($coll)) {
218 1
                $updateData['$unset'][$mapping['name']] = true;
219 1
                $this->uow->unscheduleCollectionDeletion($coll);
220
            }
221
            // @ReferenceMany is handled by CollectionPersister
222
        }
223
224 235
        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 86
    public function prepareUpsertData($document)
235
    {
236 86
        $class     = $this->dm->getClassMetadata(get_class($document));
237 86
        $changeset = $this->uow->getDocumentChangeSet($document);
238
239 86
        $updateData = [];
240 86
        foreach ($changeset as $fieldName => $change) {
241 86
            $mapping = $class->fieldMappings[$fieldName];
242
243 86
            [$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 86
            if (! isset($mapping['association'])) {
247 86
                if ($new !== null) {
248 86
                    if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) {
249 3
                        $operator = '$inc';
250 3
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new - $old);
251
                    } else {
252 86
                        $operator = '$set';
253 86
                        $value    = Type::getType($mapping['type'])->convertToDatabaseValue($new);
254
                    }
255
256 86
                    $updateData[$operator][$mapping['name']] = $value;
257 11
                } elseif ($mapping['nullable'] === true) {
258 86
                    $updateData['$setOnInsert'][$mapping['name']] = null;
259
                }
260
261
            // @EmbedOne
262 27
            } 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 8
                if ($new && $this->uow->isScheduledForInsert($new)) {
266 5
                    $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 3
                } 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 8
                    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 24
            } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
279 13
                if (isset($new) || $mapping['nullable'] === true) {
280 13
                    $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 15
            } elseif ($mapping['type'] === ClassMetadata::MANY && ! $mapping['isInverseSide']
285 15
                    && $new instanceof PersistentCollectionInterface && $new->isDirty()
286 15
                    && CollectionHelper::isAtomic($mapping['strategy'])) {
287 1
                $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 86
        if (isset($class->discriminatorField)) {
294 5
            $discriminatorValue = $class->discriminatorValue;
295
296 5
            if ($discriminatorValue === null) {
297
                if (! empty($class->discriminatorMap)) {
298
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
299
                }
300
301
                $discriminatorValue = $class->name;
302
            }
303
304 5
            $updateData['$set'][$class->discriminatorField] = $discriminatorValue;
305
        }
306
307 86
        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 222
    public function prepareReferencedDocumentValue(array $referenceMapping, $document)
322
    {
323 222
        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 189
    public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false)
349
    {
350 189
        $embeddedDocumentValue = [];
351 189
        $class                 = $this->dm->getClassMetadata(get_class($embeddedDocument));
352
353 189
        foreach ($class->fieldMappings as $mapping) {
354
            // Skip notSaved fields
355 187
            if (! empty($mapping['notSaved'])) {
356 1
                continue;
357
            }
358
359
            // Inline ClassMetadata::getFieldValue()
360 187
            $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument);
361
362 187
            $value = null;
363
364 187
            if ($rawValue !== null) {
365 184
                switch ($mapping['association'] ?? null) {
366
                    // @Field, @String, @Date, etc.
367
                    case null:
368 176
                        $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
369 176
                        break;
370
371 75
                    case ClassMetadata::EMBED_ONE:
372 70
                    case ClassMetadata::REFERENCE_ONE:
373
                        // Nested collections should only be included for embedded relationships
374 24
                        $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded']));
375 24
                        break;
376
377 53
                    case ClassMetadata::EMBED_MANY:
378 4
                    case ClassMetadata::REFERENCE_MANY:
379
                        // Skip PersistentCollections already scheduled for deletion
380 53
                        if (! $includeNestedCollections && $rawValue instanceof PersistentCollectionInterface
381 53
                            && $this->uow->isCollectionScheduledForDeletion($rawValue)) {
382
                            break;
383
                        }
384
385 53
                        $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
386 53
                        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 187
            if ($value === null && $mapping['nullable'] === false) {
395 65
                continue;
396
            }
397
398 184
            $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 189
        if (! isset($embeddedMapping['targetDocument'])) {
405 16
            $discriminatorField = $embeddedMapping['discriminatorField'];
406 16
            if (! empty($embeddedMapping['discriminatorMap'])) {
407 5
                $discriminatorValue = array_search($class->name, $embeddedMapping['discriminatorMap']);
408
409 5
                if ($discriminatorValue === false) {
410 5
                    throw MappingException::unlistedClassInDiscriminatorMap($class->name);
411
                }
412
            } else {
413 11
                $discriminatorValue = $class->name;
414
            }
415
416 15
            $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 188
        if (isset($class->discriminatorField)) {
424 8
            $discriminatorValue = $class->discriminatorValue;
425
426 8
            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 6
            $embeddedDocumentValue[$class->discriminatorField] = $discriminatorValue;
435
        }
436
437
        // Ensure empty embedded documents are stored as BSON objects
438 186
        if (empty($embeddedDocumentValue)) {
439 8
            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 182
        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 24
    public function prepareAssociatedDocumentValue(array $mapping, $document, $includeNestedCollections = false)
462
    {
463 24
        if (isset($mapping['embedded'])) {
464 11
            return $this->prepareEmbeddedDocumentValue($mapping, $document, $includeNestedCollections);
465
        }
466
467 18
        if (isset($mapping['reference'])) {
468 18
            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 232
    public function prepareAssociatedCollectionValue(PersistentCollectionInterface $coll, $includeNestedCollections = false)
482
    {
483 232
        $mapping  = $coll->getMapping();
484 232
        $pb       = $this;
485 232
        $callback = isset($mapping['embedded'])
486
            ? static function ($v) use ($pb, $mapping, $includeNestedCollections) {
487 125
                return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections);
488 129
            }
489
            : static function ($v) use ($pb, $mapping) {
490 117
                return $pb->prepareReferencedDocumentValue($mapping, $v);
491 232
            };
492
493 232
        $setData = $coll->map($callback)->toArray();
494 227
        if (CollectionHelper::isList($mapping['strategy'])) {
495 206
            $setData = array_values($setData);
496
        }
497
498 227
        $this->uow->unscheduleCollectionDeletion($coll);
499 227
        $this->uow->unscheduleCollectionUpdate($coll);
500
501 227
        return $setData;
502
    }
503
}
504