Completed
Pull Request — master (#1331)
by Maciej
10:31
created

CollectionPersister::getPathAndParent()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

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