Passed
Push — master ( 9f7d35...3f1c7e )
by Wilmer
08:50 queued 06:32
created

BatchQueryResult::next()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 10

Importance

Changes 0
Metric Value
cc 10
eloc 14
nc 12
nop 0
dl 0
loc 19
ccs 13
cts 13
cp 1
crap 10
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query;
6
7
use Yiisoft\Db\Data\DataReader;
8
use Yiisoft\Db\Connection\Connection;
9
use Yiisoft\Db\Exception\Exception;
10
11
/**
12
 * BatchQueryResult represents a batch query from which you can retrieve data in batches.
13
 *
14
 * You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by calling {@see Query::batch()} or
15
 * {@see Query::each()}. Because BatchQueryResult implements the {@see \Iterator} interface, you can iterate it to
16
 * obtain a batch of data in each iteration.
17
 *
18
 * For example,
19
 *
20
 * ```php
21
 * $query = (new Query)->from('user');
22
 * foreach ($query->batch() as $i => $users) {
23
 *     // $users represents the rows in the $i-th batch
24
 * }
25
 * foreach ($query->each() as $user) {
26
 * }
27
 * ```
28
 */
29
class BatchQueryResult implements \Iterator
30
{
31
    private int $batchSize = 100;
32
    private ?Connection $db = null;
33
    private bool $each = false;
34
    private $key;
35
    private ?Query $query = null;
36
37
    /**
38
     * @var DataReader the data reader associated with this batch query.
39
     */
40
    private ?DataReader $dataReader = null;
41
42
    /**
43
     * @var array the data retrieved in the current batch
44
     */
45
    private ?array $batch = null;
46
47
    /**
48
     * @var mixed the value for the current iteration
49
     */
50
    private $value;
51
52
    /**
53
     * @var int MSSQL error code for exception that is thrown when last batch is size less than specified batch size
54
     *
55
     * {@see https://github.com/yiisoft/yii2/issues/10023}
56
     */
57
    private int $mssqlNoMoreRowsErrorCode = -13;
58
59 12
    public function __destruct()
60
    {
61 12
        $this->reset();
62 12
    }
63
64
    /**
65
     * Resets the batch query.
66
     *
67
     * This method will clean up the existing batch query so that a new batch query can be performed.
68
     */
69 12
    public function reset(): void
70
    {
71 12
        if ($this->dataReader !== null) {
72 12
            $this->dataReader->close();
73
        }
74
75 12
        $this->dataReader = null;
76 12
        $this->batch = null;
77 12
        $this->value = null;
78 12
        $this->key = null;
79 12
    }
80
81
    /**
82
     * Resets the iterator to the initial state.
83
     *
84
     * This method is required by the interface {@see \Iterator}.
85
     */
86 12
    public function rewind(): void
87
    {
88 12
        $this->reset();
89 12
        $this->next();
90 12
    }
91
92
    /**
93
     * Moves the internal pointer to the next dataset.
94
     *
95
     * This method is required by the interface {@see \Iterator}.
96
     */
97 12
    public function next(): void
98
    {
99 12
        if ($this->batch === null || !$this->each || ($this->each && next($this->batch) === false)) {
100 12
            $this->batch = $this->fetchData();
101 12
            reset($this->batch);
102
        }
103
104 12
        if ($this->each) {
105 4
            $this->value = current($this->batch);
106 4
            if ($this->query->getIndexBy() !== null) {
0 ignored issues
show
Bug introduced by
The method getIndexBy() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

106
            if ($this->query->/** @scrutinizer ignore-call */ getIndexBy() !== null) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
107 4
                $this->key = key($this->batch);
108 4
            } elseif (key($this->batch) !== null) {
109 4
                $this->key = $this->key === null ? 0 : $this->key + 1;
110
            } else {
111 4
                $this->key = null;
112
            }
113
        } else {
114 12
            $this->value = $this->batch;
115 12
            $this->key = $this->key === null ? 0 : $this->key + 1;
116
        }
117 12
    }
118
119
    /**
120
     * Fetches the next batch of data.
121
     *
122
     * @return array the data fetched
123
     *
124
     * @throws Exception
125
     */
126 12
    protected function fetchData(): array
127
    {
128 12
        if ($this->dataReader === null) {
129 12
            $this->dataReader = $this->query->createCommand()->query();
130
        }
131
132 12
        $rows = $this->getRows();
133
134 12
        return $this->query->populate($rows);
135
    }
136
137
    /**
138
     * Reads and collects rows for batch
139
     *
140
     * @return array
141
     */
142 12
    protected function getRows(): array
143
    {
144 12
        $rows = [];
145 12
        $count = 0;
146
147
        try {
148 12
            while ($count++ < $this->batchSize && ($row = $this->dataReader->read())) {
0 ignored issues
show
Bug introduced by
The method read() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

148
            while ($count++ < $this->batchSize && ($row = $this->dataReader->/** @scrutinizer ignore-call */ read())) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
149 12
                $rows[] = $row;
150
            }
151 3
        } catch (\PDOException $e) {
152 3
            $errorCode = $e->errorInfo[1] ?? null;
153 3
            if ($this->getDbDriverName() !== 'sqlsrv' || $errorCode !== $this->mssqlNoMoreRowsErrorCode) {
154
                throw $e;
155
            }
156
        }
157
158 12
        return $rows;
159
    }
160
161
    /**
162
     * Returns the index of the current dataset.
163
     *
164
     * This method is required by the interface {@see \Iterator}.
165
     *
166
     * @return string|int|null the index of the current row.
167
     */
168 4
    public function key()
169
    {
170 4
        return $this->key;
171
    }
172
173
    /**
174
     * Returns the current dataset.
175
     *
176
     * This method is required by the interface {@see \Iterator}.
177
     *
178
     * @return mixed the current dataset.
179
     */
180 12
    public function current()
181
    {
182 12
        return $this->value;
183
    }
184
185
    /**
186
     * Returns whether there is a valid dataset at the current position.
187
     *
188
     * This method is required by the interface {@see Iterator}.
189
     *
190
     * @return bool whether there is a valid dataset at the current position.
191
     */
192 12
    public function valid(): bool
193
    {
194 12
        return !empty($this->batch);
195
    }
196
197
    /**
198
     * Gets db driver name from the db connection that is passed to the `batch()` or `each()`.
199
     *
200
     * @return string|null
201
     */
202 3
    private function getDbDriverName(): ?string
203
    {
204 3
        return $this->db->getDriverName();
0 ignored issues
show
Bug introduced by
The method getDriverName() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

204
        return $this->db->/** @scrutinizer ignore-call */ getDriverName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
205
    }
206
207
    /**
208
     * {@see Query}
209
     *
210
     * @return Query
211
     */
212 4
    public function getQuery(): Query
213
    {
214 4
        return $this->query;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->query could return the type null which is incompatible with the type-hinted return Yiisoft\Db\Query\Query. Consider adding an additional type-check to rule them out.
Loading history...
215
    }
216
217
    /**
218
     * {@see batchSize}
219
     *
220
     * @return int
221
     */
222 4
    public function getBatchSize(): int
223
    {
224 4
        return $this->batchSize;
225
    }
226
227
    /**
228
     * @param Query $value the query object associated with this batch query. Do not modify this property directly
229
     * unless after {@see reset()} is called explicitly.
230
     *
231
     * @return self
232
     */
233 12
    public function query(Query $value): self
234
    {
235 12
        $this->query = $value;
236
237 12
        return $this;
238
    }
239
240
    /**
241
     * @param int $value the number of rows to be returned in each batch.
242
     *
243
     * @return self
244
     */
245 12
    public function batchSize(int $value): self
246
    {
247 12
        $this->batchSize = $value;
248
249 12
        return $this;
250
    }
251
252
    /**
253
     * @param Connection $value the DB connection to be used when performing batch query.
254
     *
255
     * @return self
256
     */
257 12
    public function db(Connection $value): self
258
    {
259 12
        $this->db = $value;
260
261 12
        return $this;
262
    }
263
264
    /**
265
     * @param bool $value whether to return a single row during each iteration.
266
     *
267
     * If false, a whole batch of rows will be returned in each iteration.
268
     *
269
     * @return self
270
     */
271 12
    public function each(bool $value): self
272
    {
273 12
        $this->each = $value;
274
275 12
        return $this;
276
    }
277
}
278