Failed Conditions
Pull Request — master (#4007)
by Sergei
62:58
created

MysqliStatement::iterateNumeric()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\Mysqli;
6
7
use Doctrine\DBAL\Driver\DriverException;
8
use Doctrine\DBAL\Driver\FetchUtils;
9
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
10
use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
11
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
12
use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownType;
13
use Doctrine\DBAL\Driver\Statement;
14
use Doctrine\DBAL\Exception\InvalidArgumentException;
15
use Doctrine\DBAL\ParameterType;
16
use mysqli;
17
use mysqli_stmt;
18
use stdClass;
19
use Traversable;
20
use function array_combine;
21
use function array_fill;
22
use function array_map;
23
use function assert;
24
use function count;
25
use function feof;
26
use function fread;
27
use function get_resource_type;
28
use function is_array;
29
use function is_int;
30
use function is_resource;
31
use function str_repeat;
32
33
final class MysqliStatement implements Statement
34
{
35
    /** @var string[] */
36
    private static $paramTypeMap = [
37
        ParameterType::STRING       => 's',
38
        ParameterType::BINARY       => 's',
39
        ParameterType::BOOLEAN      => 'i',
40
        ParameterType::NULL         => 's',
41
        ParameterType::INTEGER      => 'i',
42
        ParameterType::LARGE_OBJECT => 'b',
43
    ];
44
45
    /** @var mysqli */
46
    private $conn;
47
48
    /** @var mysqli_stmt */
49
    private $stmt;
50
51
    /**
52
     * Whether the statement result metadata has been fetched.
53
     *
54
     * @var bool
55
     */
56
    private $metadataFetched = false;
57
58
    /**
59
     * Whether the statement result has columns. The property should be used only after the result metadata
60
     * has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined.
61
     *
62
     * @var bool
63
     */
64
    private $hasColumns = false;
65
66
    /**
67
     * Mapping of statement result column indexes to their names. The property should be used only
68
     * if the statement result has columns ({@see $hasColumns}). Otherwise, the property value is undetermined.
69
     *
70
     * @var array<int,string>
71
     */
72
    private $columnNames = [];
73
74
    /** @var mixed[] */
75
    private $rowBoundValues = [];
76
77
    /** @var mixed[] */
78
    private $boundValues = [];
79
80
    /** @var string */
81
    private $types;
82
83
    /**
84
     * Contains ref values for bindValue().
85
     *
86
     * @var mixed[]
87
     */
88
    private $values = [];
89
90
    /**
91
     * Indicates whether the statement is in the state when fetching results is possible
92
     *
93
     * @var bool
94
     */
95
    private $result = false;
96
97
    /**
98
     * @throws MysqliException
99
     */
100
    public function __construct(mysqli $conn, string $sql)
101
    {
102
        $this->conn = $conn;
103
104
        $stmt = $conn->prepare($sql);
105 2072
106
        if ($stmt === false) {
107 2072
            throw ConnectionError::new($this->conn);
108
        }
109 2072
110
        $this->stmt = $stmt;
111 2072
112 24
        $paramCount = $this->stmt->param_count;
113
        if (0 >= $paramCount) {
114
            return;
115 2048
        }
116
117 2048
        $this->types       = str_repeat('s', $paramCount);
118 2048
        $this->boundValues = array_fill(1, $paramCount, null);
119 1652
    }
120
121
    /**
122 876
     * {@inheritdoc}
123 876
     */
124 876
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
125
    {
126
        assert(is_int($param));
127
128
        if (! isset(self::$paramTypeMap[$type])) {
129 144
            throw UnknownType::new($type);
130
        }
131 144
132
        $this->boundValues[$param] =& $variable;
133 144
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
134
    }
135
136
    /**
137 144
     * {@inheritdoc}
138 144
     */
139 144
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
140
    {
141
        assert(is_int($param));
142
143
        if (! isset(self::$paramTypeMap[$type])) {
144 738
            throw UnknownType::new($type);
145
        }
146 738
147
        $this->values[$param]      = $value;
148 738
        $this->boundValues[$param] =& $this->values[$param];
149
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
150
    }
151
152 738
    /**
153 738
     * {@inheritdoc}
154 738
     */
155 738
    public function execute(?array $params = null) : void
156
    {
157
        if ($params !== null && count($params) > 0) {
158
            if (! $this->bindUntypedValues($params)) {
159
                throw StatementError::new($this->stmt);
160 2006
            }
161
        } else {
162 2006
            $this->bindTypedParameters();
163 24
        }
164 24
165
        if (! $this->stmt->execute()) {
166
            throw StatementError::new($this->stmt);
167 1994
        }
168
169
        if (! $this->metadataFetched) {
170 2000
            $meta = $this->stmt->result_metadata();
171 36
            if ($meta !== false) {
172
                $this->hasColumns = true;
173
174 1994
                $fields = $meta->fetch_fields();
175 1994
                assert(is_array($fields));
176 1994
177 1880
                $this->columnNames = array_map(static function (stdClass $field) : string {
178
                    return $field->name;
179 1880
                }, $fields);
180 1880
181
                $meta->free();
182
            } else {
183 1880
                $this->hasColumns = false;
184 1880
            }
185
186 1880
            $this->metadataFetched = true;
187
        }
188 632
189
        if ($this->hasColumns) {
190
            // Store result of every execution which has it. Otherwise it will be impossible
191 1994
            // to execute a new statement in case if the previous one has non-fetched rows
192
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
193
            $this->stmt->store_result();
194 1994
195
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
196
            // it will have to allocate as much memory as it may be needed for the given column type
197
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
198 1880
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
199
            //
200
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
201
            // previously called on the statement, the values are unbound making the statement unusable.
202
            //
203
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
204
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
205
            // to the length of the ones fetched during the previous execution.
206
            $this->rowBoundValues = array_fill(0, count($this->columnNames), null);
207
208
            $refs = [];
209
            foreach ($this->rowBoundValues as $key => &$value) {
210
                $refs[$key] =& $value;
211 1880
            }
212
213 1880
            if (! $this->stmt->bind_result(...$refs)) {
214 1880
                throw StatementError::new($this->stmt);
215 1880
            }
216
        }
217
218 1880
        $this->result = true;
219
    }
220
221
    /**
222
     * Binds parameters with known types previously bound to the statement
223 1994
     *
224 1994
     * @throws DriverException
225
     */
226
    private function bindTypedParameters() : void
227
    {
228
        $streams = $values = [];
229
        $types   = $this->types;
230
231 1994
        foreach ($this->boundValues as $parameter => $value) {
232
            assert(is_int($parameter));
233 1994
            if (! isset($types[$parameter - 1])) {
234 1994
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
235
            }
236 1994
237 870
            if ($types[$parameter - 1] === self::$paramTypeMap[ParameterType::LARGE_OBJECT]) {
238 870
                if (is_resource($value)) {
239
                    if (get_resource_type($value) !== 'stream') {
240
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
241
                    }
242 870
243 42
                    $streams[$parameter] = $value;
244 18
                    $values[$parameter]  = null;
245
                    continue;
246
                }
247
248 18
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
249 18
            }
250 18
251
            $values[$parameter] = $value;
252
        }
253 30
254
        if (count($values) > 0 && ! $this->stmt->bind_param($types, ...$values)) {
255
            throw StatementError::new($this->stmt);
256 864
        }
257
258
        $this->sendLongData($streams);
259 1994
    }
260
261
    /**
262
     * Handle $this->_longData after regular query parameters have been bound
263 1994
     *
264 1994
     * @param array<int, resource> $streams
265
     *
266
     * @throws MysqliException
267
     */
268
    private function sendLongData(array $streams) : void
269
    {
270
        foreach ($streams as $paramNr => $stream) {
271
            while (! feof($stream)) {
272
                $chunk = fread($stream, 8192);
273 1994
274
                if ($chunk === false) {
275 1994
                    throw FailedReadingStreamOffset::new($paramNr);
276 18
                }
277 18
278
                if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
279 18
                    throw StatementError::new($this->stmt);
280
                }
281
            }
282
        }
283 18
    }
284
285
    /**
286
     * Binds a array of values to bound parameters.
287
     *
288 1994
     * @param mixed[] $values
289
     */
290
    private function bindUntypedValues(array $values) : bool
291
    {
292
        $params = [];
293
        $types  = str_repeat('s', count($values));
294
295 24
        foreach ($values as &$v) {
296
            $params[] =& $v;
297 24
        }
298 24
299
        return $this->stmt->bind_param($types, ...$params);
300 24
    }
301 24
302
    /**
303
     * @return mixed[]|false|null
304 24
     */
305
    private function _fetch()
306
    {
307
        $ret = $this->stmt->fetch();
308
309
        if ($ret === true) {
310 1844
            $values = [];
311
            foreach ($this->rowBoundValues as $v) {
312 1844
                $values[] = $v;
313
            }
314 1844
315 1778
            return $values;
316 1778
        }
317 1778
318
        return $ret;
319
    }
320 1778
321
    /**
322
     * {@inheritdoc}
323 884
     */
324
    public function fetchNumeric()
325
    {
326
        // do not try fetching from the statement if it's not expected to contain the result
327
        // in order to prevent exceptional situation
328
        if (! $this->result) {
329 1898
            return false;
330
        }
331
332
        $values = $this->_fetch();
333 1898
334 54
        if ($values === null) {
335
            return false;
336
        }
337 1844
338
        if ($values === false) {
339 1844
            throw StatementError::new($this->stmt);
340 6
        }
341
342
        return $values;
343 1844
    }
344
345 1844
    /**
346 884
     * {@inheritDoc}
347
     */
348
    public function fetchAssociative()
349 1778
    {
350
        $values = $this->fetchNumeric();
351
352
        if ($values === false) {
353 1778
            return false;
354 1256
        }
355
356
        $row = array_combine($this->columnNames, $values);
357 1010
        assert(is_array($row));
358 1010
359
        return $row;
360 1010
    }
361
362 998
    /**
363
     * {@inheritdoc}
364
     */
365 12
    public function fetchOne()
366
    {
367
        return FetchUtils::fetchOne($this);
368
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373
    public function fetchAllNumeric() : array
374
    {
375 800
        return FetchUtils::fetchAllNumeric($this);
376
    }
377 800
378
    /**
379 800
     * {@inheritdoc}
380
     */
381 800
    public function fetchAllAssociative() : array
382 60
    {
383 60
        return FetchUtils::fetchAllAssociative($this);
384
    }
385
386 740
    /**
387 686
     * {@inheritdoc}
388
     */
389
    public function fetchColumn() : array
390
    {
391 800
        return FetchUtils::fetchColumn($this);
392
    }
393
394
    /**
395
     * @return Traversable<int,array<int,mixed>>
396
     *
397 1250
     * @throws DriverException
398
     */
399 1250
    public function iterateNumeric() : Traversable
400
    {
401 1250
        return FetchUtils::iterateNumeric($this);
402 96
    }
403
404
    /**
405 1214
     * @return Traversable<int,array<string,mixed>>
406
     *
407
     * @throws DriverException
408 114
     */
409
    public function iterateAssociative() : Traversable
410 114
    {
411 114
        return FetchUtils::iterateAssociative($this);
412 114
    }
413
414 606
    /**
415
     * @return Traversable<int,mixed>
416 606
     *
417
     * @throws DriverException
418
     */
419
    public function iterateColumn() : Traversable
420 606
    {
421
        return FetchUtils::iterateColumn($this);
422
    }
423 24
424
    public function closeCursor() : void
425 24
    {
426
        $this->stmt->free_result();
427
        $this->result = false;
428 1952
    }
429
430 1952
    public function rowCount() : int
431 1952
    {
432
        if ($this->hasColumns) {
433
            return $this->stmt->num_rows;
434
        }
435
436 6
        return $this->stmt->affected_rows;
437
    }
438 6
439
    public function columnCount() : int
440
    {
441
        return $this->stmt->field_count;
442
    }
443
}
444