Completed
Push — master ( cc3868...bfc8bb )
by Marco
21s queued 15s
created

MysqliStatement   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Test Coverage

Coverage 63.72%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 59
eloc 150
dl 0
loc 421
ccs 137
cts 215
cp 0.6372
rs 4.08
c 3
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
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
A getIterator() 0 3 1
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
    private $types;
86
87
    /**
88
     * Contains ref values for bindValue().
89
     *
90
     * @var mixed[]
91
     */
92
    private $values = [];
93
94
    /** @var int */
95
    private $defaultFetchMode = FetchMode::MIXED;
96
97
    /**
98
     * Indicates whether the statement is in the state when fetching results is possible
99
     *
100
     * @var bool
101
     */
102
    private $result = false;
103
104
    /**
105
     * @throws MysqliException
106
     */
107 2433
    public function __construct(mysqli $conn, string $sql)
108
    {
109 2433
        $this->conn = $conn;
110
111 2433
        $stmt = $conn->prepare($sql);
112
113 2433
        if ($stmt === false) {
114 28
            throw ConnectionError::new($this->conn);
115
        }
116
117 2405
        $this->stmt = $stmt;
118
119 2405
        $paramCount = $this->stmt->param_count;
120 2405
        if (0 >= $paramCount) {
121 1936
            return;
122
        }
123
124 1036
        $this->types       = str_repeat('s', $paramCount);
125 1036
        $this->boundValues = array_fill(1, $paramCount, null);
126 1036
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 168
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
132
    {
133 168
        assert(is_int($param));
134
135 168
        if (! isset(self::$paramTypeMap[$type])) {
136
            throw UnknownType::new($type);
137
        }
138
139 168
        $this->boundValues[$param] =& $variable;
140 168
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
141 168
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 245
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
147
    {
148 245
        assert(is_int($param));
149
150 245
        if (! isset(self::$paramTypeMap[$type])) {
151
            throw UnknownType::new($type);
152
        }
153
154 245
        $this->values[$param]      = $value;
155 245
        $this->boundValues[$param] =& $this->values[$param];
156 245
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
157 245
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 2356
    public function execute(?array $params = null) : void
163
    {
164 2356
        if ($params !== null && count($params) > 0) {
165 672
            if (! $this->bindUntypedValues($params)) {
166 672
                throw StatementError::new($this->stmt);
167
            }
168
        } else {
169 2160
            $this->bindTypedParameters();
170
        }
171
172 2349
        if (! $this->stmt->execute()) {
173 42
            throw StatementError::new($this->stmt);
174
        }
175
176 2342
        if (! $this->metadataFetched) {
177 2342
            $meta = $this->stmt->result_metadata();
178 2342
            if ($meta !== false) {
179 2209
                $this->hasColumns = true;
180
181 2209
                $fields = $meta->fetch_fields();
182 2209
                assert(is_array($fields));
183
184
                $this->columnNames = array_map(static function (stdClass $field) : string {
185 2209
                    return $field->name;
186 2209
                }, $fields);
187
188 2209
                $meta->free();
189
            } else {
190 753
                $this->hasColumns = false;
191
            }
192
193 2342
            $this->metadataFetched = true;
194
        }
195
196 2342
        if ($this->hasColumns) {
197
            // 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 2209
            $this->stmt->store_result();
201
202
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
203
            // 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
            // 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
            // to the length of the ones fetched during the previous execution.
213 2209
            $this->rowBoundValues = array_fill(0, count($this->columnNames), null);
214
215 2209
            $refs = [];
216 2209
            foreach ($this->rowBoundValues as $key => &$value) {
217 2209
                $refs[$key] =& $value;
218
            }
219
220 2209
            if (! $this->stmt->bind_result(...$refs)) {
221
                throw StatementError::new($this->stmt);
222
            }
223
        }
224
225 2342
        $this->result = true;
226 2342
    }
227
228
    /**
229
     * Binds parameters with known types previously bound to the statement
230
     *
231
     * @throws DriverException
232
     */
233 2160
    private function bindTypedParameters() : void
234
    {
235 2160
        $streams = $values = [];
236 2160
        $types   = $this->types;
237
238 2160
        foreach ($this->boundValues as $parameter => $value) {
239 413
            if (! isset($types[$parameter - 1])) {
240
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
241
            }
242
243 413
            if ($types[$parameter - 1] === self::$paramTypeMap[ParameterType::LARGE_OBJECT]) {
244 49
                if (is_resource($value)) {
245 21
                    if (get_resource_type($value) !== 'stream') {
246
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
247
                    }
248 21
                    $streams[$parameter] = $value;
249 21
                    $values[$parameter]  = null;
250 21
                    continue;
251
                }
252
253 35
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
254
            }
255
256 406
            $values[$parameter] = $value;
257
        }
258
259 2160
        if (count($values) > 0 && ! $this->stmt->bind_param($types, ...$values)) {
260
            throw StatementError::new($this->stmt);
261
        }
262
263 2160
        $this->sendLongData($streams);
264 2160
    }
265
266
    /**
267
     * Handle $this->_longData after regular query parameters have been bound
268
     *
269
     * @param resource[] $streams
270
     *
271
     * @throws MysqliException
272
     */
273 2160
    private function sendLongData(array $streams) : void
274
    {
275 2160
        foreach ($streams as $paramNr => $stream) {
276 21
            while (! feof($stream)) {
277 21
                $chunk = fread($stream, 8192);
278
279 21
                if ($chunk === false) {
280
                    throw FailedReadingStreamOffset::new($paramNr);
281
                }
282
283 21
                if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
284
                    throw StatementError::new($this->stmt);
285
                }
286
            }
287
        }
288 2160
    }
289
290
    /**
291
     * Binds a array of values to bound parameters.
292
     *
293
     * @param mixed[] $values
294
     */
295 672
    private function bindUntypedValues(array $values) : bool
296
    {
297 672
        $params = [];
298 672
        $types  = str_repeat('s', count($values));
299
300 672
        foreach ($values as &$v) {
301 672
            $params[] =& $v;
302
        }
303
304 672
        return $this->stmt->bind_param($types, ...$params);
305
    }
306
307
    /**
308
     * @return mixed[]|false|null
309
     */
310 2153
    private function _fetch()
311
    {
312 2153
        $ret = $this->stmt->fetch();
313
314 2153
        if ($ret === true) {
315 2076
            $values = [];
316 2076
            foreach ($this->rowBoundValues as $v) {
317 2076
                $values[] = $v;
318
            }
319
320 2076
            return $values;
321
        }
322
323 1012
        return $ret;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 2216
    public function fetch(?int $fetchMode = null, ...$args)
330
    {
331
        // do not try fetching from the statement if it's not expected to contain result
332
        // in order to prevent exceptional situation
333 2216
        if (! $this->result) {
334 63
            return false;
335
        }
336
337 2153
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
338
339 2153
        if ($fetchMode === FetchMode::COLUMN) {
340 7
            return $this->fetchColumn();
341
        }
342
343 2153
        $values = $this->_fetch();
344
345 2153
        if ($values === null) {
346 1012
            return false;
347
        }
348
349 2076
        if ($values === false) {
350
            throw StatementError::new($this->stmt);
351
        }
352
353 2076
        if ($fetchMode === FetchMode::NUMERIC) {
354 1460
            return $values;
355
        }
356
357 1159
        $assoc = array_combine($this->columnNames, $values);
358 1159
        assert(is_array($assoc));
359
360 1159
        switch ($fetchMode) {
361
            case FetchMode::ASSOCIATIVE:
362 1138
                return $assoc;
363
364
            case FetchMode::MIXED:
365 14
                return $assoc + $values;
366
367
            case FetchMode::STANDARD_OBJECT:
368 7
                return (object) $assoc;
369
370
            default:
371
                throw UnknownFetchMode::new($fetchMode);
372
        }
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 914
    public function fetchAll(?int $fetchMode = null, ...$args) : array
379
    {
380 914
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
381
382 914
        $rows = [];
383
384 914
        if ($fetchMode === FetchMode::COLUMN) {
385 70
            while (($row = $this->fetchColumn()) !== false) {
386 70
                $rows[] = $row;
387
            }
388
        } else {
389 844
            while (($row = $this->fetch($fetchMode)) !== false) {
390 781
                $rows[] = $row;
391
            }
392
        }
393
394 914
        return $rows;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400 1453
    public function fetchColumn(int $columnIndex = 0)
401
    {
402 1453
        $row = $this->fetch(FetchMode::NUMERIC);
403
404 1453
        if ($row === false) {
405 112
            return false;
406
        }
407
408 1411
        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 14
            throw InvalidColumnIndex::new($columnIndex, count($row));
410
        }
411
412 1397
        return $row[$columnIndex];
413
    }
414
415
    /**
416
     * {@inheritdoc}
417
     */
418 133
    public function closeCursor() : void
419
    {
420 133
        $this->stmt->free_result();
421 133
        $this->result = false;
422 133
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427 721
    public function rowCount() : int
428
    {
429 721
        if ($this->hasColumns) {
430
            return $this->stmt->num_rows;
431
        }
432
433 721
        return $this->stmt->affected_rows;
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439 28
    public function columnCount() : int
440
    {
441 28
        return $this->stmt->field_count;
442
    }
443
444
    /**
445
     * {@inheritdoc}
446
     */
447 2293
    public function setFetchMode(int $fetchMode, ...$args) : void
448
    {
449 2293
        $this->defaultFetchMode = $fetchMode;
450 2293
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455 7
    public function getIterator()
456
    {
457 7
        return new StatementIterator($this);
458
    }
459
}
460