Issues (902)

framework/db/BatchQueryResult.php (1 issue)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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
     * MSSQL error code for exception that is thrown when last batch is size less than specified batch size
46
     * @see https://github.com/yiisoft/yii2/issues/10023
47
     */
48
    const MSSQL_NO_MORE_ROWS_ERROR_CODE = -13;
49
50
    /**
51
     * @var Connection|null the DB connection to be used when performing batch query.
52
     * If null, the "db" application component will be used.
53
     */
54
    public $db;
55
    /**
56
     * @var Query the query object associated with this batch query.
57
     * Do not modify this property directly unless after [[reset()]] is called explicitly.
58
     */
59
    public $query;
60
    /**
61
     * @var int the number of rows to be returned in each batch.
62
     */
63
    public $batchSize = 100;
64
    /**
65
     * @var bool whether to return a single row during each iteration.
66
     * If false, a whole batch of rows will be returned in each iteration.
67
     */
68
    public $each = false;
69
70
    /**
71
     * @var DataReader the data reader associated with this batch query.
72
     */
73
    private $_dataReader;
74
    /**
75
     * @var array the data retrieved in the current batch
76
     */
77
    private $_batch;
78
    /**
79
     * @var mixed the value for the current iteration
80
     */
81
    private $_value;
82
    /**
83
     * @var string|int the key for the current iteration
84
     */
85
    private $_key;
86
87
88
    /**
89
     * Destructor.
90
     */
91 12
    public function __destruct()
92
    {
93
        // make sure cursor is closed
94 12
        $this->reset();
95
    }
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
    }
112
113
    /**
114
     * Resets the iterator to the initial state.
115
     * This method is required by the interface [[\Iterator]].
116
     */
117 12
    #[\ReturnTypeWillChange]
118
    public function rewind()
119
    {
120 12
        $this->reset();
121 12
        $this->next();
122
    }
123
124
    /**
125
     * Moves the internal pointer to the next dataset.
126
     * This method is required by the interface [[\Iterator]].
127
     */
128 12
    #[\ReturnTypeWillChange]
129
    public function next()
130
    {
131 12
        if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
132 12
            $this->_batch = $this->fetchData();
133 12
            reset($this->_batch);
134
        }
135
136 12
        if ($this->each) {
137 3
            $this->_value = current($this->_batch);
138 3
            if ($this->query->indexBy !== null) {
139 3
                $this->_key = key($this->_batch);
140 3
            } elseif (key($this->_batch) !== null) {
141 3
                $this->_key = $this->_key === null ? 0 : $this->_key + 1;
142
            } else {
143 3
                $this->_key = null;
144
            }
145
        } else {
146 12
            $this->_value = $this->_batch;
147 12
            $this->_key = $this->_key === null ? 0 : $this->_key + 1;
148
        }
149
    }
150
151
    /**
152
     * Fetches the next batch of data.
153
     * @return array the data fetched
154
     * @throws Exception
155
     */
156 12
    protected function fetchData()
157
    {
158 12
        if ($this->_dataReader === null) {
159 12
            $this->_dataReader = $this->query->createCommand($this->db)->query();
160
        }
161
162 12
        $rows = $this->getRows();
163
164 12
        return $this->query->populate($rows);
165
    }
166
167
    /**
168
     * Reads and collects rows for batch
169
     * @return array
170
     * @since 2.0.23
171
     */
172 12
    protected function getRows()
173
    {
174 12
        $rows = [];
175 12
        $count = 0;
176
177
        try {
178 12
            while ($count++ < $this->batchSize) {
179 12
                if ($row = $this->_dataReader->read()) {
180 12
                    $rows[] = $row;
181
                } else {
182
                    // we've reached the end
183 12
                    $this->trigger(self::EVENT_FINISH);
184 12
                    break;
185
                }
186
            }
187
        } catch (\PDOException $e) {
188
            $errorCode = isset($e->errorInfo[1]) ? $e->errorInfo[1] : null;
189
            if ($this->getDbDriverName() !== 'sqlsrv' || $errorCode !== self::MSSQL_NO_MORE_ROWS_ERROR_CODE) {
190
                throw $e;
191
            }
192
        }
193
194 12
        return $rows;
195
    }
196
197
    /**
198
     * Returns the index of the current dataset.
199
     * This method is required by the interface [[\Iterator]].
200
     * @return int the index of the current row.
201
     */
202 3
    #[\ReturnTypeWillChange]
203
    public function key()
204
    {
205 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...
206
    }
207
208
    /**
209
     * Returns the current dataset.
210
     * This method is required by the interface [[\Iterator]].
211
     * @return mixed the current dataset.
212
     */
213 12
    #[\ReturnTypeWillChange]
214
    public function current()
215
    {
216 12
        return $this->_value;
217
    }
218
219
    /**
220
     * Returns whether there is a valid dataset at the current position.
221
     * This method is required by the interface [[\Iterator]].
222
     * @return bool whether there is a valid dataset at the current position.
223
     */
224 12
    #[\ReturnTypeWillChange]
225
    public function valid()
226
    {
227 12
        return !empty($this->_batch);
228
    }
229
230
    /**
231
     * Gets db driver name from the db connection that is passed to the `batch()`, if it is not passed it uses
232
     * connection from the active record model
233
     * @return string|null
234
     */
235
    private function getDbDriverName()
236
    {
237
        if (isset($this->db->driverName)) {
238
            return $this->db->driverName;
239
        }
240
241
        if (!empty($this->_batch)) {
242
            $key = array_keys($this->_batch)[0];
243
            if (isset($this->_batch[$key]->db->driverName)) {
244
                return $this->_batch[$key]->db->driverName;
245
            }
246
        }
247
248
        return null;
249
    }
250
251
    /**
252
     * Unserialization is disabled to prevent remote code execution in case application
253
     * calls unserialize() on user input containing specially crafted string.
254
     * @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15148
255
     * @since 2.0.38
256
     */
257
    public function __wakeup()
258
    {
259
        throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
260
    }
261
}
262