Completed
Pull Request — master (#1709)
by Andreas
16:45 queued 14:38
created

CollectionPersister   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 92.39%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 8
dl 0
loc 247
ccs 85
cts 92
cp 0.9239
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A delete() 0 13 3
A setCollection() 0 9 1
B deleteElements() 0 26 3
A getPathAndParent() 0 20 4
A executeQuery() 0 15 4
C update() 0 29 8
B insertElements() 0 35 5
1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Persisters;
4
5
use Doctrine\ODM\MongoDB\DocumentManager;
6
use Doctrine\ODM\MongoDB\LockException;
7
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
8
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
9
use Doctrine\ODM\MongoDB\UnitOfWork;
10
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
11
12
/**
13
 * The CollectionPersister is responsible for persisting collections of embedded
14
 * or referenced documents. When a PersistentCollection is scheduledForDeletion
15
 * in the UnitOfWork by calling PersistentCollection::clear() or is
16
 * de-referenced in the domain application code, CollectionPersister::delete()
17
 * will be called. When documents within the PersistentCollection are added or
18
 * removed, CollectionPersister::update() will be called, which may set the
19
 * entire collection or delete/insert individual elements, depending on the
20
 * mapping strategy.
21
 *
22
 * @since       1.0
23
 */
24
class CollectionPersister
25
{
26
    /**
27
     * The DocumentManager instance.
28
     *
29
     * @var DocumentManager
30
     */
31
    private $dm;
32
33
    /**
34
     * The PersistenceBuilder instance.
35
     *
36
     * @var PersistenceBuilder
37
     */
38
    private $pb;
39
40
    /**
41
     * Constructs a new CollectionPersister instance.
42
     *
43
     * @param DocumentManager $dm
44
     * @param PersistenceBuilder $pb
45
     * @param UnitOfWork $uow
46
     */
47 1084
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
48
    {
49 1084
        $this->dm = $dm;
50 1084
        $this->pb = $pb;
51 1084
        $this->uow = $uow;
0 ignored issues
show
Bug introduced by
The property uow does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

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