Completed
Pull Request — master (#1887)
by
unknown
14:44 queued 07:29
created

PersistenceBuilder::prepareUpsertData()   D

Complexity

Conditions 27
Paths 39

Size

Total Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 27.9208

Importance

Changes 0
Metric Value
dl 0
loc 68
ccs 33
cts 37
cp 0.8919
rs 4.1666
c 0
b 0
f 0
cc 27
nc 39
nop 1
crap 27.9208

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