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