Completed
Pull Request — master (#1880)
by
unknown
19:45
created

CollectionPersister::deleteAll()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5.0406

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 15
cts 17
cp 0.8824
rs 9.2248
c 0
b 0
f 0
cc 5
nc 7
nop 2
crap 5.0406
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 LogicException;
14
use UnexpectedValueException;
15
use function array_map;
16
use function array_reverse;
17
use function array_values;
18
use function get_class;
19
use function implode;
20
use function sprintf;
21
22
/**
23
 * The CollectionPersister is responsible for persisting collections of embedded
24
 * or referenced documents. When a PersistentCollection is scheduledForDeletion
25
 * in the UnitOfWork by calling PersistentCollection::clear() or is
26
 * de-referenced in the domain application code, CollectionPersister::delete()
27
 * will be called. When documents within the PersistentCollection are added or
28
 * removed, CollectionPersister::update() will be called, which may set the
29
 * entire collection or delete/insert individual elements, depending on the
30
 * mapping strategy.
31
 */
32
class CollectionPersister
33
{
34
    /** @var DocumentManager */
35
    private $dm;
36
37
    /** @var PersistenceBuilder */
38
    private $pb;
39
40
    /** @var UnitOfWork */
41
    private $uow;
42
43 1124
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
44
    {
45 1124
        $this->dm  = $dm;
46 1124
        $this->pb  = $pb;
47 1124
        $this->uow = $uow;
48 1124
    }
49
50
    /**
51
     * Deletes a PersistentCollection instances completely from a document using $unset. If collections belong to the different
52
     *
53
     * @param PersistentCollectionInterface[] $collections
54
     * @param array $options
55
     */
56 34
    public function deleteAll(array $collections, array $options)
57
    {
58 34
        $parents = [];
59 34
        $unsetPathsMap = [];
60
61 34
        foreach ($collections as $coll) {
62 34
            $mapping = $coll->getMapping();
63 34
            if ($mapping['isInverseSide']) {
64
                continue; // ignore inverse side
65
            }
66 34
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
67
                throw new \UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
68
            }
69 34
            list($propertyPath, $parent) = $this->getPathAndParent($coll);
70 34
            $oid = \spl_object_hash($parent);
71 34
            $parents[$oid] = $parent;
72 34
            $unsetPathsMap[$oid][$propertyPath] = true;
73
        }
74
75 34
        foreach ($unsetPathsMap as $oid => $unsetPaths) {
76 34
            $query = array('$unset' => $unsetPaths);
77 34
            $this->executeQuery($parents[$oid], $query, $options);
78
        }
79 34
    }
80
81
    /**
82
     * Deletes a PersistentCollection instance completely from a document using $unset.
83
     */
84 4
    public function delete(PersistentCollectionInterface $coll, array $options) : void
85
    {
86 4
        $mapping = $coll->getMapping();
87 4
        if ($mapping['isInverseSide']) {
88
            return; // ignore inverse side
89
        }
90 4
        if (CollectionHelper::isAtomic($mapping['strategy'])) {
91
            throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
92
        }
93 4
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath 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 $parent 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...
94 4
        $query                   = ['$unset' => [$propertyPath => true]];
95 4
        $this->executeQuery($parent, $query, $options);
96 3
    }
97
98
    /**
99
     * Updates a PersistentCollection instance deleting removed rows and
100
     * inserting new rows.
101
     */
102 100
    public function update(PersistentCollectionInterface $coll, array $options) : void
103
    {
104 100
        $mapping = $coll->getMapping();
105
106 100
        if ($mapping['isInverseSide']) {
107
            return; // ignore inverse side
108
        }
109
110 100
        switch ($mapping['strategy']) {
111
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
112
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
113
                throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
114
115
            case ClassMetadata::STORAGE_STRATEGY_SET:
116
            case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
117 10
                $this->setCollection($coll, $options);
118 10
                break;
119
120
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
121
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
122 91
                $coll->initialize();
123 91
                $this->deleteElements($coll, $options);
124 91
                $this->insertElements($coll, $options);
125 90
                break;
126
127
            default:
128
                throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
129
        }
130 99
    }
131
132
    /**
133
     * Sets a PersistentCollection instance.
134
     *
135
     * This method is intended to be used with the "set" or "setArray"
136
     * strategies. The "setArray" strategy will ensure that the collection is
137
     * set as a BSON array, which means the collection elements will be
138
     * reindexed numerically before storage.
139
     */
140 10
    private function setCollection(PersistentCollectionInterface $coll, array $options) : void
141
    {
142 10
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath 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 $parent 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...
143 10
        $coll->initialize();
144 10
        $mapping = $coll->getMapping();
145 10
        $setData = $this->pb->prepareAssociatedCollectionValue($coll, CollectionHelper::usesSet($mapping['strategy']));
146 10
        $query   = ['$set' => [$propertyPath => $setData]];
147 10
        $this->executeQuery($parent, $query, $options);
148 10
    }
149
150
    /**
151
     * Deletes removed elements from a PersistentCollection instance.
152
     *
153
     * This method is intended to be used with the "pushAll" and "addToSet"
154
     * strategies.
155
     */
156 91
    private function deleteElements(PersistentCollectionInterface $coll, array $options) : void
157
    {
158 91
        $deleteDiff = $coll->getDeleteDiff();
159
160 91
        if (empty($deleteDiff)) {
161 71
            return;
162
        }
163
164 29
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath 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 $parent 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...
165
166 29
        $query = ['$unset' => []];
167
168 29
        foreach ($deleteDiff as $key => $document) {
169 29
            $query['$unset'][$propertyPath . '.' . $key] = true;
170
        }
171
172 29
        $this->executeQuery($parent, $query, $options);
173
174
        /**
175
         * @todo This is a hack right now because we don't have a proper way to
176
         * remove an element from an array by its key. Unsetting the key results
177
         * in the element being left in the array as null so we have to pull
178
         * null values.
179
         */
180 29
        $this->executeQuery($parent, ['$pull' => [$propertyPath => null]], $options);
181 29
    }
182
183
    /**
184
     * Inserts new elements for a PersistentCollection instance.
185
     *
186
     * This method is intended to be used with the "pushAll" and "addToSet"
187
     * strategies.
188
     */
189 91
    private function insertElements(PersistentCollectionInterface $coll, array $options) : void
190
    {
191 91
        $insertDiff = $coll->getInsertDiff();
192
193 91
        if (empty($insertDiff)) {
194 21
            return;
195
        }
196
197 76
        $mapping = $coll->getMapping();
198
199 76
        switch ($mapping['strategy']) {
200
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
201 72
                $operator = 'push';
202 72
                break;
203
204
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
205 6
                $operator = 'addToSet';
206 6
                break;
207
208
            default:
209
                throw new LogicException(sprintf('Invalid strategy %s given for insertElements', $mapping['strategy']));
210
        }
211
212 76
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath 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 $parent 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...
213
214 76
        $callback = isset($mapping['embedded'])
215
            ? function ($v) use ($mapping) {
216 40
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
217 40
            }
218
            : function ($v) use ($mapping) {
219 37
                return $this->pb->prepareReferencedDocumentValue($mapping, $v);
220 76
            };
221
222 76
        $value = array_values(array_map($callback, $insertDiff));
223
224 76
        $query = ['$' . $operator => [$propertyPath => ['$each' => $value]]];
225
226 76
        $this->executeQuery($parent, $query, $options);
227 75
    }
228
229
    /**
230
     * Gets the parent information for a given PersistentCollection. It will
231
     * retrieve the top-level persistent Document that the PersistentCollection
232
     * lives in. We can use this to issue queries when updating a
233
     * PersistentCollection that is multiple levels deep inside an embedded
234
     * document.
235
     *
236
     *     <code>
237
     *     list($path, $parent) = $this->getPathAndParent($coll)
238
     *     </code>
239
     */
240 114
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
241
    {
242 114
        $mapping = $coll->getMapping();
243 114
        $fields  = [];
244 114
        $parent  = $coll->getOwner();
245 114
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
246 16
            [$m, $owner, $field] = $association;
0 ignored issues
show
Bug introduced by
The variable $m 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 $owner 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 $field does not exist. Did you mean $fields?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
247 16
            if (isset($m['reference'])) {
248
                break;
249
            }
250 16
            $parent   = $owner;
251 16
            $fields[] = $field;
0 ignored issues
show
Bug introduced by
The variable $field does not exist. Did you mean $fields?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
252
        }
253 114
        $propertyPath = implode('.', array_reverse($fields));
254 114
        $path         = $mapping['name'];
255 114
        if ($propertyPath) {
256 16
            $path = $propertyPath . '.' . $path;
257
        }
258 114
        return [$path, $parent];
259
    }
260
261
    /**
262
     * Executes a query updating the given document.
263
     */
264 114
    private function executeQuery(object $document, array $newObj, array $options) : void
265
    {
266 114
        $className = get_class($document);
267 114
        $class     = $this->dm->getClassMetadata($className);
268 114
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
269 114
        $query     = ['_id' => $id];
270 114
        if ($class->isVersioned) {
271 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
272
        }
273 114
        $collection = $this->dm->getDocumentCollection($className);
274 114
        $result     = $collection->updateOne($query, $newObj, $options);
275 114
        if ($class->isVersioned && ! $result->getMatchedCount()) {
276 2
            throw LockException::lockFailed($document);
277
        }
278 112
    }
279
}
280