Completed
Pull Request — master (#1887)
by
unknown
15:03 queued 09:19
created

PersistenceBuilder   F

Complexity

Total Complexity 117

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 94.76%

Importance

Changes 0
Metric Value
wmc 117
lcom 1
cbo 6
dl 0
loc 464
ccs 181
cts 191
cp 0.9476
rs 2
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
C prepareInsertData() 0 54 17
F prepareUpdateData() 0 100 42
D prepareUpsertData() 0 71 28
A prepareReferencedDocumentValue() 0 4 1
A prepareAssociatedDocumentValue() 0 12 3
A prepareAssociatedCollectionValue() 0 22 3
F prepareEmbeddedDocumentValue() 0 97 22

How to fix   Complexity   

Complex Class

Complex classes like PersistenceBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PersistenceBuilder, and based on these observations, apply Extract Interface, too.

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