Completed
Push — master ( 48b0b5...26ecbc )
by Andreas
16:25 queued 10s
created

ODM/MongoDB/Persisters/CollectionPersister.php (8 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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