Completed
Pull Request — master (#1787)
by Stefano
15:28
created

CollectionPersister   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 92.71%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 7
dl 0
loc 249
ccs 89
cts 96
cp 0.9271
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getPathAndParent() 0 20 4
A executeQuery() 0 15 4
B deleteElements() 0 26 3
A __construct() 0 6 1
A delete() 0 13 3
C update() 0 29 8
A setCollection() 0 9 1
B insertElements() 0 39 5
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\LockException;
9
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
10
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
11
use Doctrine\ODM\MongoDB\UnitOfWork;
12
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
13
use function array_map;
14
use function array_reverse;
15
use function array_values;
16
use function get_class;
17
use function implode;
18
use function sprintf;
19
20
/**
21
 * The CollectionPersister is responsible for persisting collections of embedded
22
 * or referenced documents. When a PersistentCollection is scheduledForDeletion
23
 * in the UnitOfWork by calling PersistentCollection::clear() or is
24
 * de-referenced in the domain application code, CollectionPersister::delete()
25
 * will be called. When documents within the PersistentCollection are added or
26
 * removed, CollectionPersister::update() will be called, which may set the
27
 * entire collection or delete/insert individual elements, depending on the
28
 * mapping strategy.
29
 *
30
 */
31
class CollectionPersister
32
{
33
    /**
34
     * The DocumentManager instance.
35
     *
36
     * @var DocumentManager
37
     */
38
    private $dm;
39
40
    /**
41
     * The PersistenceBuilder instance.
42
     *
43
     * @var PersistenceBuilder
44
     */
45
    private $pb;
46
47
    /**
48
     * The UnitOfWork instance.
49
     *
50
     * @var UnitOfWork
51
     */
52
    private $uow;
53
54
    /**
55
     * Constructs a new CollectionPersister instance.
56
     *
57
     */
58 1074
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
59
    {
60 1074
        $this->dm = $dm;
61 1074
        $this->pb = $pb;
62 1074
        $this->uow = $uow;
63 1074
    }
64
65
    /**
66
     * Deletes a PersistentCollection instance completely from a document using $unset.
67
     *
68
     * @param array $options
69
     */
70 34
    public function delete(PersistentCollectionInterface $coll, array $options)
71
    {
72 34
        $mapping = $coll->getMapping();
73 34
        if ($mapping['isInverseSide']) {
74
            return; // ignore inverse side
75
        }
76 34
        if (CollectionHelper::isAtomic($mapping['strategy'])) {
77
            throw new \UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
78
        }
79 34
        list($propertyPath, $parent) = $this->getPathAndParent($coll);
80 34
        $query = ['$unset' => [$propertyPath => true]];
81 34
        $this->executeQuery($parent, $query, $options);
82 33
    }
83
84
    /**
85
     * Updates a PersistentCollection instance deleting removed rows and
86
     * inserting new rows.
87
     *
88
     * @param array $options
89
     */
90 98
    public function update(PersistentCollectionInterface $coll, array $options)
91
    {
92 98
        $mapping = $coll->getMapping();
93
94 98
        if ($mapping['isInverseSide']) {
95
            return; // ignore inverse side
96
        }
97
98 98
        switch ($mapping['strategy']) {
99
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
100
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
101
                throw new \UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
102
103
            case ClassMetadata::STORAGE_STRATEGY_SET:
104
            case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
105 10
                $this->setCollection($coll, $options);
106 10
                break;
107
108
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
109
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
110 89
                $coll->initialize();
111 89
                $this->deleteElements($coll, $options);
112 89
                $this->insertElements($coll, $options);
113 88
                break;
114
115
            default:
116
                throw new \UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
117
        }
118 97
    }
119
120
    /**
121
     * Sets a PersistentCollection instance.
122
     *
123
     * This method is intended to be used with the "set" or "setArray"
124
     * strategies. The "setArray" strategy will ensure that the collection is
125
     * set as a BSON array, which means the collection elements will be
126
     * reindexed numerically before storage.
127
     *
128
     * @param array $options
129
     */
130 10
    private function setCollection(PersistentCollectionInterface $coll, array $options)
131
    {
132 10
        list($propertyPath, $parent) = $this->getPathAndParent($coll);
133 10
        $coll->initialize();
134 10
        $mapping = $coll->getMapping();
135 10
        $setData = $this->pb->prepareAssociatedCollectionValue($coll, CollectionHelper::usesSet($mapping['strategy']));
136 10
        $query = ['$set' => [$propertyPath => $setData]];
137 10
        $this->executeQuery($parent, $query, $options);
138 10
    }
139
140
    /**
141
     * Deletes removed elements from a PersistentCollection instance.
142
     *
143
     * This method is intended to be used with the "pushAll" and "addToSet"
144
     * strategies.
145
     *
146
     * @param array $options
147
     */
148 89
    private function deleteElements(PersistentCollectionInterface $coll, array $options)
149
    {
150 89
        $deleteDiff = $coll->getDeleteDiff();
151
152 89
        if (empty($deleteDiff)) {
153 69
            return;
154
        }
155
156 29
        list($propertyPath, $parent) = $this->getPathAndParent($coll);
157
158 29
        $query = ['$unset' => []];
159
160 29
        foreach ($deleteDiff as $key => $document) {
161 29
            $query['$unset'][$propertyPath . '.' . $key] = true;
162
        }
163
164 29
        $this->executeQuery($parent, $query, $options);
165
166
        /**
167
         * @todo This is a hack right now because we don't have a proper way to
168
         * remove an element from an array by its key. Unsetting the key results
169
         * in the element being left in the array as null so we have to pull
170
         * null values.
171
         */
172 29
        $this->executeQuery($parent, ['$pull' => [$propertyPath => null]], $options);
173 29
    }
174
175
    /**
176
     * Inserts new elements for a PersistentCollection instance.
177
     *
178
     * This method is intended to be used with the "pushAll" and "addToSet"
179
     * strategies.
180
     *
181
     * @param array $options
182
     */
183 89
    private function insertElements(PersistentCollectionInterface $coll, array $options)
184
    {
185 89
        $insertDiff = $coll->getInsertDiff();
186
187 89
        if (empty($insertDiff)) {
188 21
            return;
189
        }
190
191 74
        $mapping = $coll->getMapping();
192
193 74
        switch ($mapping['strategy']) {
194
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
195 70
                $operator = 'push';
196 70
                break;
197
198
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
199 6
                $operator = 'addToSet';
200 6
                break;
201
202
            default:
203
                throw new \LogicException(sprintf('Invalid strategy %s given for insertElements', $mapping['strategy']));
204
        }
205
206 74
        list($propertyPath, $parent) = $this->getPathAndParent($coll);
207
208 74
        $callback = isset($mapping['embedded'])
209
            ? function ($v) use ($mapping) {
210 38
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
211 38
            }
212
            : function ($v) use ($mapping) {
213 37
                return $this->pb->prepareReferencedDocumentValue($mapping, $v);
214 74
            };
215
216 74
        $value = array_values(array_map($callback, $insertDiff));
217
218 74
        $query = ['$' . $operator => [$propertyPath => ['$each' => $value]]];
219
220 74
        $this->executeQuery($parent, $query, $options);
221 73
    }
222
223
    /**
224
     * Gets the parent information for a given PersistentCollection. It will
225
     * retrieve the top-level persistent Document that the PersistentCollection
226
     * lives in. We can use this to issue queries when updating a
227
     * PersistentCollection that is multiple levels deep inside an embedded
228
     * document.
229
     *
230
     *     <code>
231
     *     list($path, $parent) = $this->getPathAndParent($coll)
232
     *     </code>
233
     *
234
     * @return array $pathAndParent
235
     */
236 108
    private function getPathAndParent(PersistentCollectionInterface $coll)
237
    {
238 108
        $mapping = $coll->getMapping();
239 108
        $fields = [];
240 108
        $parent = $coll->getOwner();
241 108
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
242 14
            list($m, $owner, $field) = $association;
243 14
            if (isset($m['reference'])) {
244
                break;
245
            }
246 14
            $parent = $owner;
247 14
            $fields[] = $field;
248
        }
249 108
        $propertyPath = implode('.', array_reverse($fields));
250 108
        $path = $mapping['name'];
251 108
        if ($propertyPath) {
252 14
            $path = $propertyPath . '.' . $path;
253
        }
254 108
        return [$path, $parent];
255
    }
256
257
    /**
258
     * Executes a query updating the given document.
259
     *
260
     * @param object $document
261
     * @param array  $newObj
262
     * @param array  $options
263
     */
264 108
    private function executeQuery($document, array $newObj, array $options)
265
    {
266 108
        $className = get_class($document);
267 108
        $class = $this->dm->getClassMetadata($className);
268 108
        $id = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
269 108
        $query = ['_id' => $id];
270 108
        if ($class->isVersioned) {
271 3
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
272
        }
273 108
        $collection = $this->dm->getDocumentCollection($className);
274 108
        $result = $collection->updateOne($query, $newObj, $options);
275 108
        if ($class->isVersioned && ! $result->getMatchedCount()) {
276 2
            throw LockException::lockFailed($document);
277
        }
278 106
    }
279
}
280