Completed
Push — master ( 030f70...fd4f15 )
by Andreas
23:15
created

MongoWriteBatch::convertWriteErrors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 8
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 13
rs 9.4285
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 33 and the first side effect is on line 17.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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
16
if (class_exists('MongoWriteBatch', false)) {
17
    return;
18
}
19
20
use Alcaeus\MongoDbAdapter\TypeConverter;
21
use Alcaeus\MongoDbAdapter\Helper\WriteConcernConverter;
22
use MongoDB\Driver\Exception\BulkWriteException;
23
use MongoDB\Driver\WriteError;
24
use MongoDB\Driver\WriteResult;
25
26
/**
27
 * MongoWriteBatch allows you to "batch up" multiple operations (of same type)
28
 * and shipping them all to MongoDB at the same time. This can be especially
29
 * useful when operating on many documents at the same time to reduce roundtrips.
30
 *
31
 * @see http://php.net/manual/en/class.mongowritebatch.php
32
 */
33
class MongoWriteBatch
34
{
35
    use WriteConcernConverter;
36
37
    const COMMAND_INSERT = 1;
38
    const COMMAND_UPDATE = 2;
39
    const COMMAND_DELETE = 3;
40
41
    /**
42
     * @var MongoCollection
43
     */
44
    private $collection;
45
46
    /**
47
     * @var int
48
     */
49
    private $batchType;
50
51
    /**
52
     * @var array
53
     */
54
    private $writeOptions;
55
56
    /**
57
     * @var array
58
     */
59
    private $items = [];
60
61
    /**
62
     * Creates a new batch of write operations
63
     *
64
     * @see http://php.net/manual/en/mongowritebatch.construct.php
65
     * @param MongoCollection $collection
66
     * @param int $batchType
67
     * @param array $writeOptions
68
     */
69
    protected function __construct(MongoCollection $collection, $batchType, $writeOptions)
70
    {
71
        $this->collection = $collection;
72
        $this->batchType = $batchType;
73
        $this->writeOptions = $writeOptions;
74
    }
75
76
    /**
77
     * Adds a write operation to a batch
78
     *
79
     * @see http://php.net/manual/en/mongowritebatch.add.php
80
     * @param array|object $item
81
     * @return boolean
82
     */
83
    public function add($item)
84
    {
85
        if (is_object($item)) {
86
            $item = (array)$item;
87
        }
88
89
        $this->validate($item);
90
        $this->addItem($item);
91
92
        return true;
93
    }
94
95
    /**
96
     * Executes a batch of write operations
97
     *
98
     * @see http://php.net/manual/en/mongowritebatch.execute.php
99
     * @param array $writeOptions
100
     * @return array
101
     */
102
    final public function execute(array $writeOptions = [])
103
    {
104
        $writeOptions += $this->writeOptions;
105
        if (! count($this->items)) {
106
            return ['ok' => true];
107
        }
108
109
        if (isset($writeOptions['j'])) {
110
            trigger_error('j parameter is not supported', E_WARNING);
111
        }
112
        if (isset($writeOptions['fsync'])) {
113
            trigger_error('fsync parameter is not supported', E_WARNING);
114
        }
115
116
        $options['writeConcern'] = $this->createWriteConcernFromArray($writeOptions);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $options = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
117
        if (isset($writeOptions['ordered'])) {
118
            $options['ordered'] = $writeOptions['ordered'];
119
        }
120
121
        try {
122
            $writeResult = $this->collection->getCollection()->bulkWrite($this->items, $options);
123
            $resultDocument = [];
124
            $ok = true;
125
        } catch (BulkWriteException $e) {
126
            $writeResult = $e->getWriteResult();
127
            $resultDocument = ['writeErrors' => $this->convertWriteErrors($writeResult)];
128
            $ok = false;
129
        }
130
131
        $this->items = [];
132
133
        switch ($this->batchType) {
134
            case self::COMMAND_UPDATE:
135
                $upsertedIds = [];
136
                foreach ($writeResult->getUpsertedIds() as $index => $id) {
137
                    $upsertedIds[] = [
138
                        'index' => $index,
139
                        '_id' => TypeConverter::toLegacy($id)
140
                    ];
141
                }
142
143
                $resultDocument += [
144
                    'nMatched' => $writeResult->getMatchedCount(),
145
                    'nModified' => $writeResult->getModifiedCount(),
146
                    'nUpserted' => $writeResult->getUpsertedCount(),
147
                    'ok' => true,
148
                ];
149
150
                if (count($upsertedIds)) {
151
                    $resultDocument['upserted'] = $upsertedIds;
152
                }
153
                break;
154
155
            case self::COMMAND_DELETE:
156
                $resultDocument += [
157
                    'nRemoved' => $writeResult->getDeletedCount(),
158
                    'ok' => true,
159
                ];
160
                break;
161
162
            case self::COMMAND_INSERT:
163
                $resultDocument += [
164
                    'nInserted' => $writeResult->getInsertedCount(),
165
                    'ok' => true,
166
                ];
167
                break;
168
        }
169
170
        if (! $ok) {
171
            // Exception code is hardcoded to the value in ext-mongo, see
172
            // https://github.com/mongodb/mongo-php-driver-legacy/blob/ab4bc0d90e93b3f247f6bcb386d0abc8d2fa7d74/batch/write.c#L428
173
            throw new \MongoWriteConcernException('Failed write', 911, null, $resultDocument);
174
        }
175
176
        return $resultDocument;
177
    }
178
179
    private function validate(array $item)
180
    {
181
        switch ($this->batchType) {
182 View Code Duplication
            case self::COMMAND_UPDATE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
                if (! isset($item['q'])) {
184
                    throw new Exception("Expected \$item to contain 'q' key");
185
                }
186
                if (! isset($item['u'])) {
187
                    throw new Exception("Expected \$item to contain 'u' key");
188
                }
189
                break;
190
191 View Code Duplication
            case self::COMMAND_DELETE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
192
                if (! isset($item['q'])) {
193
                    throw new Exception("Expected \$item to contain 'q' key");
194
                }
195
                if (! isset($item['limit'])) {
196
                    throw new Exception("Expected \$item to contain 'limit' key");
197
                }
198
                break;
199
        }
200
    }
201
202
    private function addItem(array $item)
203
    {
204
        switch ($this->batchType) {
205
            case self::COMMAND_UPDATE:
206
                $method = isset($item['multi']) ? 'updateMany' : 'updateOne';
207
208
                $options = [];
209
                if (isset($item['upsert']) && $item['upsert']) {
210
                    $options['upsert'] = true;
211
                }
212
213
                $this->items[] = [$method => [TypeConverter::fromLegacy($item['q']), TypeConverter::fromLegacy($item['u']), $options]];
214
                break;
215
216
            case self::COMMAND_INSERT:
217
                $this->items[] = ['insertOne' => [TypeConverter::fromLegacy($item)]];
218
                break;
219
220
            case self::COMMAND_DELETE:
221
                $method = $item['limit'] === 0 ? 'deleteMany' : 'deleteOne';
222
223
                $this->items[] = [$method => [TypeConverter::fromLegacy($item['q'])]];
224
                break;
225
        }
226
    }
227
228
    /**
229
     * @param WriteResult $result
230
     * @return array
231
     */
232
    private function convertWriteErrors(WriteResult $result)
233
    {
234
        $writeErrors = [];
235
        /** @var WriteError $writeError */
236
        foreach ($result->getWriteErrors() as $writeError) {
237
            $writeErrors[] = [
238
                'index' => $writeError->getIndex(),
239
                'code' => $writeError->getCode(),
240
                'errmsg' => $writeError->getMessage(),
241
            ];
242
        }
243
        return $writeErrors;
244
    }
245
}
246