Completed
Pull Request — master (#262)
by Andreas
01:39
created

AbstractCursor::rewind()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
ccs 7
cts 7
cp 1
crap 1
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
16
namespace Alcaeus\MongoDbAdapter;
17
18
use Alcaeus\MongoDbAdapter\Helper\ReadPreference;
19
use MongoDB\Collection;
20
use MongoDB\Driver\Cursor;
21
22
/**
23
 * @internal
24
 */
25
abstract class AbstractCursor
26
{
27
    use ReadPreference;
28
29
    /**
30
     * @var int|null
31
     */
32
    protected $batchSize = null;
33
34
    /**
35
     * @var Collection
36
     */
37
    protected $collection;
38
39
    /**
40
     * @var \MongoClient
41
     */
42
    protected $connection;
43
44
    /**
45
     * @var Cursor
46
     */
47
    protected $cursor;
48
49
    /**
50
     * @var \MongoDB\Database
51
     */
52
    protected $db;
53
54
    /**
55
     * @var \Iterator
56
     */
57
    protected $iterator;
58
59
    /**
60
     * @var string
61
     */
62
    protected $ns;
63
64
    /**
65
     * @var bool
66
     */
67
    protected $startedIterating = false;
68
69
    /**
70
     * @var bool
71
     */
72
    protected $cursorNeedsAdvancing = true;
73
74
    /**
75
     * @var mixed
76
     */
77
    private $current = null;
78
79
    /**
80
     * @var mixed
81
     */
82
    private $key = null;
83
84
    /**
85
     * @var mixed
86
     */
87
    private $valid = false;
88
89
    /**
90
     * @var int
91
     */
92
    protected $position = 0;
93
94
    /**
95
     * @var array
96
     */
97
    protected $optionNames = [
98
        'batchSize',
99
        'readPreference',
100
    ];
101
102
    /**
103
     * @return Cursor
104
     */
105
    abstract protected function ensureCursor();
106
107
    /**
108
     * @return array
109
     */
110
    abstract protected function getCursorInfo();
111
112
    /**
113
     * Create a new cursor
114
     * @link http://www.php.net/manual/en/mongocursor.construct.php
115
     * @param \MongoClient $connection Database connection.
116
     * @param string $ns Full name of database and collection.
117
     */
118 96
    public function __construct(\MongoClient $connection, $ns)
119
    {
120 96
        $this->connection = $connection;
121 96
        $this->ns = $ns;
122
123 96
        $nsParts = explode('.', $ns);
124 96
        $dbName = array_shift($nsParts);
125 96
        $collectionName = implode('.', $nsParts);
126
127 96
        $this->db = $connection->selectDB($dbName)->getDb();
128
129 96
        if ($collectionName) {
130 59
            $this->collection = $connection->selectCollection($dbName, $collectionName)->getCollection();
131
        }
132 96
    }
133
134 94
    public function __destruct()
135
    {
136 94
        $this->iterator = null;
137 94
    }
138
139
    /**
140
     * Returns the current element
141
     * @link http://www.php.net/manual/en/mongocursor.current.php
142
     * @return array
143
     */
144 45
    public function current()
145
    {
146 45
        return $this->current;
147
    }
148
149
    /**
150
     * Returns the current result's _id
151
     * @link http://www.php.net/manual/en/mongocursor.key.php
152
     * @return string The current result's _id as a string.
153
     */
154 29
    public function key()
155
    {
156 29
        return $this->key;
157
    }
158
159
    /**
160
     * Advances the cursor to the next result, and returns that result
161
     * @link http://www.php.net/manual/en/mongocursor.next.php
162
     * @throws \MongoConnectionException
163
     * @throws \MongoCursorTimeoutException
164
     * @return array Returns the next object
165
     */
166 47
    public function next()
167
    {
168 47 View Code Duplication
        if (! $this->startedIterating) {
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...
169 1
            $this->ensureIterator();
170 1
            $this->startedIterating = true;
171
        } else {
172 47
            if ($this->cursorNeedsAdvancing) {
173 46
                $this->ensureIterator()->next();
174
            }
175
176 47
            $this->cursorNeedsAdvancing = true;
177 47
            $this->position++;
178
        }
179
180 47
        return $this->storeIteratorState();
181
    }
182
183
    /**
184
     * Returns the cursor to the beginning of the result set
185
     * @throws \MongoConnectionException
186
     * @throws \MongoCursorTimeoutException
187
     * @return void
188
     */
189 78
    public function rewind()
190
    {
191
        // We can recreate the cursor to allow it to be rewound
192 78
        $this->reset();
193 78
        $this->startedIterating = true;
194 78
        $this->position = 0;
195 78
        $this->ensureIterator()->rewind();
196 76
        $this->storeIteratorState();
197 76
    }
198
199
    /**
200
     * Checks if the cursor is reading a valid result.
201
     * @link http://www.php.net/manual/en/mongocursor.valid.php
202
     * @return boolean If the current result is not null.
203
     */
204 76
    public function valid()
205
    {
206 76
        return $this->valid;
207
    }
208
209
    /**
210
     * Limits the number of elements returned in one batch.
211
     *
212
     * @link http://docs.php.net/manual/en/mongocursor.batchsize.php
213
     * @param int|null $batchSize The number of results to return per batch
214
     * @return $this Returns this cursor.
215
     */
216 2
    public function batchSize($batchSize)
217
    {
218 2
        $this->batchSize = $batchSize;
219
220 2
        return $this;
221
    }
222
223
    /**
224
     * Checks if there are documents that have not been sent yet from the database for this cursor
225
     * @link http://www.php.net/manual/en/mongocursor.dead.php
226
     * @return boolean Returns if there are more results that have not been sent to the client, yet.
227
     */
228
    public function dead()
229
    {
230
        return $this->ensureCursor()->isDead();
231
    }
232
233
    /**
234
     * @return array
235
     */
236 5
    public function info()
237
    {
238 5
        return $this->getCursorInfo() + $this->getIterationInfo();
239
    }
240
241
    /**
242
     * @link http://www.php.net/manual/en/mongocursor.setreadpreference.php
243
     * @param string $readPreference
244
     * @param array $tags
245
     * @return $this Returns this cursor.
246
     */
247 96
    public function setReadPreference($readPreference, $tags = null)
248
    {
249 96
        $this->setReadPreferenceFromParameters($readPreference, $tags);
250
251 96
        return $this;
252
    }
253
254
    /**
255
     * Sets a client-side timeout for this query
256
     * @link http://www.php.net/manual/en/mongocursor.timeout.php
257
     * @param int $ms The number of milliseconds for the cursor to wait for a response. By default, the cursor will wait forever.
258
     * @return $this Returns this cursor
259
     */
260
    public function timeout($ms)
0 ignored issues
show
Unused Code introduced by
The parameter $ms is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
261
    {
262
        trigger_error('The ' . __METHOD__ . ' method is not implemented in mongo-php-adapter', E_USER_WARNING);
263
        return $this;
264
    }
265
266
    /**
267
     * Applies all options set on the cursor, overwriting any options that have already been set
268
     *
269
     * @param array $optionNames Array of option names to be applied (will be read from properties)
270
     * @return array
271
     */
272 87
    protected function getOptions($optionNames = null)
273
    {
274 87
        $options = [];
275
276 87
        if ($optionNames === null) {
277 80
            $optionNames = $this->optionNames;
278
        }
279
280 87
        foreach ($optionNames as $option) {
281 87
            $converter = 'convert' . ucfirst($option);
282 87
            $value = method_exists($this, $converter) ? $this->$converter() : $this->$option;
283
284 87
            if ($value === null) {
285 87
                continue;
286
            }
287
288 82
            $options[$option] = $value;
289
        }
290
291 87
        return $options;
292
    }
293
294
    /**
295
     * @return \Iterator
296
     */
297 80
    protected function ensureIterator()
298
    {
299 80
        if ($this->iterator === null) {
300
            // MongoDB\Driver\Cursor needs to be wrapped into a \Generator so that a valid \Iterator with working implementations of
301
            // next, current, valid, key and rewind is returned. These methods don't work if we wrap the Cursor inside an \IteratorIterator
302 80
            $this->iterator = $this->wrapTraversable($this->ensureCursor());
303
        }
304
305 78
        return $this->iterator;
306
    }
307
308
    /**
309
     * @param \Traversable $traversable
310
     * @return \Generator
311
     */
312 37
    protected function wrapTraversable(\Traversable $traversable)
313
    {
314 37
        foreach ($traversable as $key => $value) {
315 23
            yield $key => $value;
316
        }
317 37
    }
318
319
    /**
320
     * @throws \MongoCursorException
321
     */
322 25
    protected function errorIfOpened()
323
    {
324 25
        if ($this->cursor === null) {
325 25
            return;
326
        }
327
328
        throw new \MongoCursorException('cannot modify cursor after beginning iteration.');
329
    }
330
331
    /**
332
     * @return array
333
     */
334 5
    protected function getIterationInfo()
335
    {
336
        $iterationInfo = [
337 5
            'started_iterating' => $this->cursor !== null,
338
        ];
339
340 5
        if ($this->cursor !== null) {
341 5
            switch ($this->cursor->getServer()->getType()) {
342
                case \MongoDB\Driver\Server::TYPE_RS_ARBITER:
343
                    $typeString = 'ARBITER';
344
                    break;
345
                case \MongoDB\Driver\Server::TYPE_MONGOS:
346
                    $typeString = 'MONGOS';
347
                    break;
348
                case \MongoDB\Driver\Server::TYPE_RS_PRIMARY:
349
                    $typeString = 'PRIMARY';
350
                    break;
351
                case \MongoDB\Driver\Server::TYPE_RS_SECONDARY:
352
                    $typeString = 'SECONDARY';
353
                    break;
354
                default:
355 5
                    $typeString = 'STANDALONE';
356
            }
357
358 5
            $cursorId = (string) $this->cursor->getId();
359
            $iterationInfo += [
360 5
                'id' => (int) $cursorId,
361 5
                'at' => $this->position,
362 5
                'numReturned' => $this->position, // This can't be obtained from the new cursor
363 5
                'server' => sprintf('%s:%d;-;.;%d', $this->cursor->getServer()->getHost(), $this->cursor->getServer()->getPort(), getmypid()),
364 5
                'host' => $this->cursor->getServer()->getHost(),
365 5
                'port' => $this->cursor->getServer()->getPort(),
366 5
                'connection_type_desc' => $typeString,
367
            ];
368
        }
369
370 5
        return $iterationInfo;
371
    }
372
373
    /**
374
     * @throws \Exception
375
     */
376
    protected function notImplemented()
377
    {
378
        throw new \Exception('Not implemented');
379
    }
380
381
    /**
382
     * Clears the cursor
383
     *
384
     * This is generic but implemented as protected since it's only exposed in MongoCursor
385
     */
386 79
    protected function reset()
387
    {
388 79
        $this->startedIterating = false;
389 79
        $this->cursorNeedsAdvancing = true;
390 79
        $this->cursor = null;
391 79
        $this->iterator = null;
392 79
        $this->storeIteratorState();
393 79
    }
394
395
    /**
396
     * @return array
397
     */
398 3
    public function __sleep()
399
    {
400 3
        return ['batchSize', 'connection', 'iterator', 'ns', 'optionNames', 'position', 'startedIterating'];
401
    }
402
403
    /**
404
     * Stores the current cursor element.
405
     *
406
     * This is necessary because hasNext() might advance the iterator but we still
407
     * need to be able to return the current object.
408
     */
409 80
    protected function storeIteratorState()
410
    {
411 80
        if (! $this->startedIterating) {
412 79
            $this->current = null;
413 79
            $this->key = null;
414 79
            $this->valid = false;
415 79
            return null;
416
        }
417
418 78
        $this->current = $this->ensureIterator()->current();
419 78
        $this->key = $this->ensureIterator()->key();
420 78
        $this->valid = $this->ensureIterator()->valid();
421
422 78
        if ($this->current !== null) {
423 47
            $this->current = TypeConverter::toLegacy($this->current);
424
        }
425
426 78
        return $this->current;
427
    }
428
}
429