Passed
Push — fix-array-access ( 050ccc...ecf3a7 )
by Alexander
56:12 queued 49:24
created

BatchQueryResult   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 224
Duplicated Lines 0 %

Test Coverage

Coverage 78.13%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 65
c 6
b 0
f 0
dl 0
loc 224
ccs 50
cts 64
cp 0.7813
rs 9.92
wmc 31

11 Methods

Rating   Name   Duplication   Size   Complexity  
A reset() 0 10 2
A key() 0 3 1
B getRows() 0 23 7
A __destruct() 0 4 1
B next() 0 19 10
A __wakeup() 0 3 1
A getDbDriverName() 0 14 4
A current() 0 3 1
A rewind() 0 4 1
A valid() 0 3 1
A fetchData() 0 9 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use yii\base\Component;
11
12
/**
13
 * BatchQueryResult represents a batch query from which you can retrieve data in batches.
14
 *
15
 * You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by
16
 * calling [[Query::batch()]] or [[Query::each()]]. Because BatchQueryResult implements the [[\Iterator]] interface,
17
 * you can iterate it to obtain a batch of data in each iteration. For example,
18
 *
19
 * ```php
20
 * $query = (new Query)->from('user');
21
 * foreach ($query->batch() as $i => $users) {
22
 *     // $users represents the rows in the $i-th batch
23
 * }
24
 * foreach ($query->each() as $user) {
25
 * }
26
 * ```
27
 *
28
 * @author Qiang Xue <[email protected]>
29
 * @since 2.0
30
 */
31
class BatchQueryResult extends Component implements \Iterator
32
{
33
    /**
34
     * @event Event an event that is triggered when the batch query is reset.
35
     * @see reset()
36
     * @since 2.0.41
37
     */
38
    const EVENT_RESET = 'reset';
39
    /**
40
     * @event Event an event that is triggered when the last batch has been fetched.
41
     * @since 2.0.41
42
     */
43
    const EVENT_FINISH = 'finish';
44
45
    /**
46
     * @var Connection the DB connection to be used when performing batch query.
47
     * If null, the "db" application component will be used.
48
     */
49
    public $db;
50
    /**
51
     * @var Query the query object associated with this batch query.
52
     * Do not modify this property directly unless after [[reset()]] is called explicitly.
53
     */
54
    public $query;
55
    /**
56
     * @var int the number of rows to be returned in each batch.
57
     */
58
    public $batchSize = 100;
59
    /**
60
     * @var bool whether to return a single row during each iteration.
61
     * If false, a whole batch of rows will be returned in each iteration.
62
     */
63
    public $each = false;
64
65
    /**
66
     * @var DataReader the data reader associated with this batch query.
67
     */
68
    private $_dataReader;
69
    /**
70
     * @var array the data retrieved in the current batch
71
     */
72
    private $_batch;
73
    /**
74
     * @var mixed the value for the current iteration
75
     */
76
    private $_value;
77
    /**
78
     * @var string|int the key for the current iteration
79
     */
80
    private $_key;
81
    /**
82
     * @var int MSSQL error code for exception that is thrown when last batch is size less than specified batch size
83
     * @see https://github.com/yiisoft/yii2/issues/10023
84
     */
85
    private $mssqlNoMoreRowsErrorCode = -13;
86
87
88
    /**
89
     * Destructor.
90
     */
91 12
    public function __destruct()
92
    {
93
        // make sure cursor is closed
94 12
        $this->reset();
95 12
    }
96
97
    /**
98
     * Resets the batch query.
99
     * This method will clean up the existing batch query so that a new batch query can be performed.
100
     */
101 12
    public function reset()
102
    {
103 12
        if ($this->_dataReader !== null) {
104 12
            $this->_dataReader->close();
105
        }
106 12
        $this->_dataReader = null;
107 12
        $this->_batch = null;
108 12
        $this->_value = null;
109 12
        $this->_key = null;
110 12
        $this->trigger(self::EVENT_RESET);
111 12
    }
112
113
    /**
114
     * Resets the iterator to the initial state.
115
     * This method is required by the interface [[\Iterator]].
116
     */
117 12
    public function rewind()
118
    {
119 12
        $this->reset();
120 12
        $this->next();
121 12
    }
122
123
    /**
124
     * Moves the internal pointer to the next dataset.
125
     * This method is required by the interface [[\Iterator]].
126
     */
127 12
    public function next()
128
    {
129 12
        if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
130 12
            $this->_batch = $this->fetchData();
131 12
            reset($this->_batch);
132
        }
133
134 12
        if ($this->each) {
135 3
            $this->_value = current($this->_batch);
136 3
            if ($this->query->indexBy !== null) {
137 3
                $this->_key = key($this->_batch);
138 3
            } elseif (key($this->_batch) !== null) {
139 3
                $this->_key = $this->_key === null ? 0 : $this->_key + 1;
140
            } else {
141 3
                $this->_key = null;
142
            }
143
        } else {
144 12
            $this->_value = $this->_batch;
145 12
            $this->_key = $this->_key === null ? 0 : $this->_key + 1;
146
        }
147 12
    }
148
149
    /**
150
     * Fetches the next batch of data.
151
     * @return array the data fetched
152
     * @throws Exception
153
     */
154 12
    protected function fetchData()
155
    {
156 12
        if ($this->_dataReader === null) {
157 12
            $this->_dataReader = $this->query->createCommand($this->db)->query();
158
        }
159
160 12
        $rows = $this->getRows();
161
162 12
        return $this->query->populate($rows);
163
    }
164
165
    /**
166
     * Reads and collects rows for batch
167
     * @return array
168
     * @since 2.0.23
169
     */
170 12
    protected function getRows()
171
    {
172 12
        $rows = [];
173 12
        $count = 0;
174
175
        try {
176 12
            while ($count++ < $this->batchSize) {
177 12
                if ($row = $this->_dataReader->read()) {
178 12
                    $rows[] = $row;
179
                } else {
180
                    // we've reached the end
181 12
                    $this->trigger(self::EVENT_FINISH);
182 12
                    break;
183
                }
184
            }
185
        } catch (\PDOException $e) {
186
            $errorCode = isset($e->errorInfo[1]) ? $e->errorInfo[1] : null;
187
            if ($this->getDbDriverName() !== 'sqlsrv' || $errorCode !== $this->mssqlNoMoreRowsErrorCode) {
188
                throw $e;
189
            }
190
        }
191
192 12
        return $rows;
193
    }
194
195
    /**
196
     * Returns the index of the current dataset.
197
     * This method is required by the interface [[\Iterator]].
198
     * @return int the index of the current row.
199
     */
200 3
    public function key()
201
    {
202 3
        return $this->_key;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_key also could return the type string which is incompatible with the documented return type integer.
Loading history...
203
    }
204
205
    /**
206
     * Returns the current dataset.
207
     * This method is required by the interface [[\Iterator]].
208
     * @return mixed the current dataset.
209
     */
210 12
    public function current()
211
    {
212 12
        return $this->_value;
213
    }
214
215
    /**
216
     * Returns whether there is a valid dataset at the current position.
217
     * This method is required by the interface [[\Iterator]].
218
     * @return bool whether there is a valid dataset at the current position.
219
     */
220 12
    public function valid()
221
    {
222 12
        return !empty($this->_batch);
223
    }
224
225
    /**
226
     * Gets db driver name from the db connection that is passed to the `batch()`, if it is not passed it uses
227
     * connection from the active record model
228
     * @return string|null
229
     */
230
    private function getDbDriverName()
231
    {
232
        if (isset($this->db->driverName)) {
233
            return $this->db->driverName;
234
        }
235
236
        if (!empty($this->_batch)) {
237
            $key = array_keys($this->_batch)[0];
238
            if (isset($this->_batch[$key]->db->driverName)) {
239
                return $this->_batch[$key]->db->driverName;
240
            }
241
        }
242
243
        return null;
244
    }
245
246
    /**
247
     * Unserialization is disabled to prevent remote code execution in case application
248
     * calls unserialize() on user input containing specially crafted string.
249
     * @see CVE-2020-15148
250
     * @since 2.0.38
251
     */
252
    public function __wakeup()
253
    {
254
        throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
255
    }
256
}
257