Failed Conditions
Pull Request — master (#3817)
by Sergei
61:27
created

MysqliStatement   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Test Coverage

Coverage 92.62%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 59
eloc 150
c 3
b 0
f 0
dl 0
loc 421
ccs 138
cts 149
cp 0.9262
rs 4.08

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getIterator() 0 3 1
A bindValue() 0 11 2
A rowCount() 0 7 2
B fetch() 0 43 10
A bindParam() 0 10 2
A closeCursor() 0 4 1
A __construct() 0 19 3
B execute() 0 64 10
A _fetch() 0 14 3
A setFetchMode() 0 3 1
A columnCount() 0 3 1
A fetchColumn() 0 13 3
A bindUntypedValues() 0 10 2
A sendLongData() 0 12 5
B bindTypedParameters() 0 31 8
A fetchAll() 0 17 5

How to fix   Complexity   

Complex Class

Complex classes like MysqliStatement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MysqliStatement, and based on these observations, apply Extract Interface, too.

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\Mysqli\Exception\ConnectionError;
9
use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
10
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
11
use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownFetchMode;
12
use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownType;
13
use Doctrine\DBAL\Driver\Statement;
14
use Doctrine\DBAL\Driver\StatementIterator;
15
use Doctrine\DBAL\Exception\InvalidArgumentException;
16
use Doctrine\DBAL\Exception\InvalidColumnIndex;
17
use Doctrine\DBAL\FetchMode;
18
use Doctrine\DBAL\ParameterType;
19
use IteratorAggregate;
20
use mysqli;
21
use mysqli_stmt;
22
use stdClass;
23
use function array_combine;
24
use function array_fill;
25
use function array_key_exists;
26
use function array_map;
27
use function assert;
28
use function count;
29
use function feof;
30
use function fread;
31
use function get_resource_type;
32
use function is_array;
33
use function is_int;
34
use function is_resource;
35
use function str_repeat;
36
37
final class MysqliStatement implements IteratorAggregate, Statement
38
{
39
    /** @var string[] */
40
    private static $paramTypeMap = [
41
        ParameterType::STRING       => 's',
42
        ParameterType::BINARY       => 's',
43
        ParameterType::BOOLEAN      => 'i',
44
        ParameterType::NULL         => 's',
45
        ParameterType::INTEGER      => 'i',
46
        ParameterType::LARGE_OBJECT => 'b',
47
    ];
48
49
    /** @var mysqli */
50
    private $conn;
51
52
    /** @var mysqli_stmt */
53
    private $stmt;
54
55
    /**
56
     * Whether the statement result metadata has been fetched.
57
     *
58
     * @var bool
59
     */
60
    private $metadataFetched = false;
61
62
    /**
63
     * Whether the statement result has columns. The property should be used only after the result metadata
64
     * has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined.
65
     *
66
     * @var bool
67
     */
68
    private $hasColumns = false;
69
70
    /**
71
     * Mapping of statement result column indexes to their names. The property should be used only
72
     * if the statement result has columns ({@see $hasColumns}). Otherwise, the property value is undetermined.
73
     *
74
     * @var array<int,string>
75
     */
76
    private $columnNames;
77
78
    /** @var mixed[] */
79
    private $rowBoundValues = [];
80
81
    /** @var mixed[] */
82
    private $boundValues = [];
83
84
    /** @var string */
85 2781
    private $types;
86
87 2781
    /**
88
     * Contains ref values for bindValue().
89 2781
     *
90
     * @var mixed[]
91 2781
     */
92 32
    private $values = [];
93
94
    /** @var int */
95 2749
    private $defaultFetchMode = FetchMode::MIXED;
96
97 2749
    /**
98 2749
     * Indicates whether the statement is in the state when fetching results is possible
99 2213
     *
100
     * @var bool
101
     */
102 1184
    private $result = false;
103 1184
104 1184
    /**
105
     * @throws MysqliException
106
     */
107
    public function __construct(mysqli $conn, string $sql)
108
    {
109 192
        $this->conn = $conn;
110
111 192
        $stmt = $conn->prepare($sql);
112
113 192
        if ($stmt === false) {
114
            throw ConnectionError::new($this->conn);
115
        }
116
117 192
        $this->stmt = $stmt;
118 192
119 192
        $paramCount = $this->stmt->param_count;
120
        if (0 >= $paramCount) {
121
            return;
122
        }
123
124 280
        $this->types       = str_repeat('s', $paramCount);
125
        $this->boundValues = array_fill(1, $paramCount, null);
126 280
    }
127
128 280
    /**
129
     * {@inheritdoc}
130
     */
131
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
132 280
    {
133 280
        assert(is_int($param));
134 280
135 280
        if (! isset(self::$paramTypeMap[$type])) {
136
            throw UnknownType::new($type);
137
        }
138
139
        $this->boundValues[$param] =& $variable;
140 2693
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
141
    }
142 2693
143 768
    /**
144 768
     * {@inheritdoc}
145
     */
146
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
147 2469
    {
148
        assert(is_int($param));
149
150 2685
        if (! isset(self::$paramTypeMap[$type])) {
151 48
            throw UnknownType::new($type);
152
        }
153
154 2677
        $this->values[$param]      = $value;
155 2677
        $this->boundValues[$param] =& $this->values[$param];
156 2677
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
157 2525
    }
158 2525
159
    /**
160 2525
     * {@inheritdoc}
161 2525
     */
162 2525
    public function execute(?array $params = null) : void
163
    {
164
        if ($params !== null && count($params) > 0) {
165 2525
            if (! $this->bindUntypedValues($params)) {
166
                throw StatementError::new($this->stmt);
167 2525
            }
168
        } else {
169 861
            $this->bindTypedParameters();
170
        }
171
172
        if (! $this->stmt->execute()) {
173 2677
            throw StatementError::new($this->stmt);
174
        }
175
176
        if (! $this->metadataFetched) {
177 2525
            $meta = $this->stmt->result_metadata();
178
            if ($meta !== false) {
179
                $this->hasColumns = true;
180
181
                $fields = $meta->fetch_fields();
182
                assert(is_array($fields));
183
184
                $this->columnNames = array_map(static function (stdClass $field) : string {
185
                    return $field->name;
186
                }, $fields);
187
188
                $meta->free();
189
            } else {
190 2525
                $this->hasColumns = false;
191
            }
192 2525
193 2525
            $this->metadataFetched = true;
194 2525
        }
195
196
        if ($this->hasColumns) {
197 2525
            // Store result of every execution which has it. Otherwise it will be impossible
198
            // to execute a new statement in case if the previous one has non-fetched rows
199
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
200
            $this->stmt->store_result();
201
202 2677
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
203 2677
            // it will have to allocate as much memory as it may be needed for the given column type
204
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
205
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
206
            //
207
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
208
            // previously called on the statement, the values are unbound making the statement unusable.
209
            //
210 2469
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
211
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
212 2469
            // to the length of the ones fetched during the previous execution.
213 2469
            $this->rowBoundValues = array_fill(0, count($this->columnNames), null);
214
215 2469
            $refs = [];
216 472
            foreach ($this->rowBoundValues as $key => &$value) {
217
                $refs[$key] =& $value;
218
            }
219
220 472
            if (! $this->stmt->bind_result(...$refs)) {
221 56
                throw StatementError::new($this->stmt);
222 24
            }
223
        }
224
225 24
        $this->result = true;
226 24
    }
227 24
228
    /**
229
     * Binds parameters with known types previously bound to the statement
230 40
     *
231
     * @throws DriverException
232
     */
233 464
    private function bindTypedParameters() : void
234
    {
235
        $streams = $values = [];
236 2469
        $types   = $this->types;
237
238
        foreach ($this->boundValues as $parameter => $value) {
239
            if (! isset($types[$parameter - 1])) {
240 2469
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
241 2469
            }
242
243
            if ($types[$parameter - 1] === self::$paramTypeMap[ParameterType::LARGE_OBJECT]) {
244
                if (is_resource($value)) {
245
                    if (get_resource_type($value) !== 'stream') {
246
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
247
                    }
248
                    $streams[$parameter] = $value;
249
                    $values[$parameter]  = null;
250 2469
                    continue;
251
                }
252 2469
253 24
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
254 24
            }
255
256 24
            $values[$parameter] = $value;
257
        }
258
259
        if (count($values) > 0 && ! $this->stmt->bind_param($types, ...$values)) {
260 24
            throw StatementError::new($this->stmt);
261
        }
262
263
        $this->sendLongData($streams);
264
    }
265 2469
266
    /**
267
     * Handle $this->_longData after regular query parameters have been bound
268
     *
269
     * @param resource[] $streams
270
     *
271
     * @throws MysqliException
272 768
     */
273
    private function sendLongData(array $streams) : void
274 768
    {
275 768
        foreach ($streams as $paramNr => $stream) {
276
            while (! feof($stream)) {
277 768
                $chunk = fread($stream, 8192);
278 768
279
                if ($chunk === false) {
280
                    throw FailedReadingStreamOffset::new($paramNr);
281 768
                }
282
283
                if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
284
                    throw StatementError::new($this->stmt);
285
                }
286
            }
287 2461
        }
288
    }
289 2461
290
    /**
291 2461
     * Binds a array of values to bound parameters.
292 2373
     *
293 2373
     * @param mixed[] $values
294 2373
     */
295
    private function bindUntypedValues(array $values) : bool
296
    {
297 2373
        $params = [];
298
        $types  = str_repeat('s', count($values));
299
300 1157
        foreach ($values as &$v) {
301
            $params[] =& $v;
302
        }
303
304
        return $this->stmt->bind_param($types, ...$params);
305
    }
306 2533
307
    /**
308
     * @return mixed[]|false|null
309
     */
310 2533
    private function _fetch()
311 72
    {
312
        $ret = $this->stmt->fetch();
313
314 2461
        if ($ret === true) {
315
            $values = [];
316 2461
            foreach ($this->rowBoundValues as $v) {
317 8
                $values[] = $v;
318
            }
319
320 2461
            return $values;
321
        }
322 2461
323 1157
        return $ret;
324
    }
325
326 2373
    /**
327
     * {@inheritdoc}
328
     */
329
    public function fetch(?int $fetchMode = null, ...$args)
330 2373
    {
331 1669
        // do not try fetching from the statement if it's not expected to contain result
332
        // in order to prevent exceptional situation
333
        if (! $this->result) {
334 1325
            return false;
335 1325
        }
336 1325
337
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
338 1325
339
        if ($fetchMode === FetchMode::COLUMN) {
340 1301
            return $this->fetchColumn();
341
        }
342
343 16
        $values = $this->_fetch();
344
345
        if ($values === null) {
346 8
            return false;
347
        }
348
349
        if ($values === false) {
350
            throw StatementError::new($this->stmt);
351
        }
352
353
        if ($fetchMode === FetchMode::NUMERIC) {
354
            return $values;
355
        }
356 1045
357
        $assoc = array_combine($this->columnNames, $values);
358 1045
        assert(is_array($assoc));
359
360 1045
        switch ($fetchMode) {
361
            case FetchMode::ASSOCIATIVE:
362 1045
                return $assoc;
363 80
364 80
            case FetchMode::MIXED:
365
                return $assoc + $values;
366
367 965
            case FetchMode::STANDARD_OBJECT:
368 893
                return (object) $assoc;
369
370
            default:
371
                throw UnknownFetchMode::new($fetchMode);
372 1045
        }
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 1661
    public function fetchAll(?int $fetchMode = null, ...$args) : array
379
    {
380 1661
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
381
382 1661
        $rows = [];
383 128
384
        if ($fetchMode === FetchMode::COLUMN) {
385
            while (($row = $this->fetchColumn()) !== false) {
386 1613
                $rows[] = $row;
387 16
            }
388
        } else {
389
            while (($row = $this->fetch($fetchMode)) !== false) {
390 1597
                $rows[] = $row;
391
            }
392
        }
393
394
        return $rows;
395
    }
396 152
397
    /**
398 152
     * {@inheritdoc}
399 152
     */
400 152
    public function fetchColumn(int $columnIndex = 0)
401
    {
402
        $row = $this->fetch(FetchMode::NUMERIC);
403
404
        if ($row === false) {
405 824
            return false;
406
        }
407 824
408 824
        if (! array_key_exists($columnIndex, $row)) {
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type object; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

408
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
409
            throw InvalidColumnIndex::new($columnIndex, count($row));
410
        }
411
412
        return $row[$columnIndex];
413
    }
414
415
    /**
416
     * {@inheritdoc}
417 32
     */
418
    public function closeCursor() : void
419 32
    {
420
        $this->stmt->free_result();
421
        $this->result = false;
422
    }
423
424
    /**
425 2621
     * {@inheritdoc}
426
     */
427 2621
    public function rowCount() : int
428 2621
    {
429
        if ($this->hasColumns) {
430
            return $this->stmt->num_rows;
431
        }
432
433 62
        return $this->stmt->affected_rows;
434
    }
435 62
436
    /**
437
     * {@inheritdoc}
438
     */
439
    public function columnCount() : int
440
    {
441
        return $this->stmt->field_count;
442
    }
443
444
    /**
445
     * {@inheritdoc}
446
     */
447
    public function setFetchMode(int $fetchMode, ...$args) : void
448
    {
449
        $this->defaultFetchMode = $fetchMode;
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455
    public function getIterator()
456
    {
457
        return new StatementIterator($this);
458
    }
459
}
460