Completed
Pull Request — master (#1418)
by
unknown
11:10 queued 07:57
created

CollectionPersister   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 239
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 93.68%

Importance

Changes 3
Bugs 0 Features 1
Metric Value
wmc 28
c 3
b 0
f 1
lcom 1
cbo 7
dl 0
loc 239
ccs 89
cts 95
cp 0.9368
rs 10

8 Methods

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