PersistenceBuilder::prepareUpdateData()   F
last analyzed

Complexity

Conditions 42
Paths 72

Size

Total Lines 100

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 100
ccs 56
cts 56
cp 1
rs 3.3333
c 0
b 0
f 0
cc 42
nc 72
nop 1
crap 42

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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