Failed Conditions
Push — master ( 94cec7...7f79d0 )
by Marco
25s queued 13s
created

MysqliStatement   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 424
Duplicated Lines 0 %

Test Coverage

Coverage 88.82%

Importance

Changes 0
Metric Value
wmc 60
eloc 155
dl 0
loc 424
ccs 135
cts 152
cp 0.8882
rs 3.6
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A _fetch() 0 14 3
A bindValue() 0 13 2
A rowCount() 0 7 2
B fetch() 0 43 10
A bindParam() 0 12 2
A closeCursor() 0 6 1
A __construct() 0 19 3
A errorInfo() 0 3 1
A getIterator() 0 3 1
A errorCode() 0 3 1
B execute() 0 67 11
A setFetchMode() 0 5 1
A columnCount() 0 3 1
A fetchColumn() 0 9 2
A bindUntypedValues() 0 10 2
A sendLongData() 0 12 5
B bindTypedParameters() 0 31 7
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
namespace Doctrine\DBAL\Driver\Mysqli;
4
5
use Doctrine\DBAL\Driver\Statement;
6
use Doctrine\DBAL\Driver\StatementIterator;
7
use Doctrine\DBAL\Exception\InvalidArgumentException;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\ParameterType;
10
use IteratorAggregate;
11
use mysqli;
12
use mysqli_stmt;
13
use PDO;
14
use function array_combine;
15
use function array_fill;
16
use function assert;
17
use function count;
18
use function feof;
19
use function fread;
20
use function get_resource_type;
21
use function is_array;
22
use function is_int;
23
use function is_resource;
24
use function sprintf;
25
use function str_repeat;
26
27
class MysqliStatement implements IteratorAggregate, Statement
28
{
29
    /** @var string[] */
30
    protected static $_paramTypeMap = [
31
        ParameterType::STRING       => 's',
32
        ParameterType::BINARY       => 's',
33
        ParameterType::BOOLEAN      => 'i',
34
        ParameterType::NULL         => 's',
35
        ParameterType::INTEGER      => 'i',
36
        ParameterType::LARGE_OBJECT => 'b',
37
    ];
38
39
    /** @var mysqli */
40
    protected $_conn;
41
42
    /** @var mysqli_stmt */
43
    protected $_stmt;
44
45
    /** @var string[]|false|null */
46
    protected $_columnNames;
47
48
    /** @var mixed[]|null */
49
    protected $_rowBindedValues;
50
51
    /** @var mixed[] */
52
    protected $_bindedValues;
53
54
    /** @var string */
55
    protected $types;
56
57
    /**
58
     * Contains ref values for bindValue().
59
     *
60
     * @var mixed[]
61
     */
62
    protected $_values = [];
63
64
    /** @var int */
65
    protected $_defaultFetchMode = FetchMode::MIXED;
66
67
    /**
68
     * Indicates whether the statement is in the state when fetching results is possible
69
     *
70
     * @var bool
71
     */
72
    private $result = false;
73
74
    /**
75
     * @param string $prepareString
76
     *
77
     * @throws MysqliException
78 1910
     */
79
    public function __construct(mysqli $conn, $prepareString)
80 1910
    {
81 1910
        $this->_conn = $conn;
82 1910
83 28
        $stmt = $conn->prepare($prepareString);
84
85
        if ($stmt === false) {
86 1882
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
87 1882
        }
88 1518
89
        $this->_stmt = $stmt;
90
91 889
        $paramCount = $this->_stmt->param_count;
92 889
        if (0 >= $paramCount) {
93 889
            return;
94
        }
95
96
        $this->types         = str_repeat('s', $paramCount);
97
        $this->_bindedValues = array_fill(1, $paramCount, null);
98 56
    }
99
100 56
    /**
101
     * {@inheritdoc}
102
     */
103 56
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
104
    {
105
        assert(is_int($column));
106
107 56
        if (! isset(self::$_paramTypeMap[$type])) {
108
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
109
        }
110 56
111 56
        $this->_bindedValues[$column] =& $variable;
112
        $this->types[$column - 1]     = self::$_paramTypeMap[$type];
113 56
114
        return true;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119 252
     */
120
    public function bindValue($param, $value, $type = ParameterType::STRING)
121 252
    {
122
        assert(is_int($param));
123
124 252
        if (! isset(self::$_paramTypeMap[$type])) {
125
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
126
        }
127
128 252
        $this->_values[$param]       = $value;
129
        $this->_bindedValues[$param] =& $this->_values[$param];
130
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
131 252
132 252
        return true;
133 252
    }
134
135 252
    /**
136
     * {@inheritdoc}
137
     */
138
    public function execute($params = null)
139
    {
140
        if ($this->_bindedValues !== null) {
141 1826
            if ($params !== null) {
142
                if (! $this->bindUntypedValues($params)) {
143 1826
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
144 889
                }
145 630
            } else {
146 630
                $this->bindTypedParameters();
147
            }
148
        }
149 308
150 308
        if (! $this->_stmt->execute()) {
151
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
152
        }
153 308
154
        if ($this->_columnNames === null) {
155
            $meta = $this->_stmt->result_metadata();
156
            if ($meta !== false) {
157 1826
                $fields = $meta->fetch_fields();
158 42
                assert(is_array($fields));
159
160
                $columnNames = [];
161 1819
                foreach ($fields as $col) {
162 1819
                    $columnNames[] = $col->name;
163 1819
                }
164 1686
165 1686
                $meta->free();
166 1686
167
                $this->_columnNames = $columnNames;
168 1686
            } else {
169
                $this->_columnNames = false;
170 1686
            }
171
        }
172 722
173
        if ($this->_columnNames !== false) {
174
            // Store result of every execution which has it. Otherwise it will be impossible
175
            // to execute a new statement in case if the previous one has non-fetched rows
176 1819
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
177
            $this->_stmt->store_result();
178
179
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
180 1686
            // it will have to allocate as much memory as it may be needed for the given column type
181
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
182
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
183
            //
184
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
185
            // previously called on the statement, the values are unbound making the statement unusable.
186
            //
187
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
188
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
189
            // to the length of the ones fetched during the previous execution.
190
            $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);
0 ignored issues
show
Bug introduced by
It seems like $this->_columnNames can also be of type true; however, parameter $var of count() does only seem to accept Countable|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

190
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
191
192
            $refs = [];
193 1686
            foreach ($this->_rowBindedValues as $key => &$value) {
194
                $refs[$key] =& $value;
195 1686
            }
196 1686
197 1686
            if (! $this->_stmt->bind_result(...$refs)) {
198
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
199
            }
200 1686
        }
201
202
        $this->result = true;
203
204
        return true;
205 1819
    }
206
207 1819
    /**
208
     * Binds parameters with known types previously bound to the statement
209
     */
210
    private function bindTypedParameters()
211
    {
212
        $streams = $values = [];
213
        $types   = $this->types;
214
215
        foreach ($this->_bindedValues as $parameter => $value) {
216 308
            if (! isset($types[$parameter - 1])) {
217
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
218 308
            }
219 308
220
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
221 308
                if (is_resource($value)) {
222 308
                    if (get_resource_type($value) !== 'stream') {
223
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
224
                    }
225
                    $streams[$parameter] = $value;
226 308
                    $values[$parameter]  = null;
227 49
                    continue;
228 21
                } else {
229
                    $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
230
                }
231 21
            }
232 21
233 21
            $values[$parameter] = $value;
234
        }
235 35
236
        if (! $this->_stmt->bind_param($types, ...$values)) {
237
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
238
        }
239 301
240
        $this->sendLongData($streams);
241
    }
242 308
243
    /**
244
     * Handle $this->_longData after regular query parameters have been bound
245
     *
246
     * @throws MysqliException
247
     */
248
    private function sendLongData($streams)
249
    {
250 308
        foreach ($streams as $paramNr => $stream) {
251
            while (! feof($stream)) {
252 308
                $chunk = fread($stream, 8192);
253 21
254 21
                if ($chunk === false) {
255
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
256 21
                }
257
258
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
259
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
260 21
                }
261
            }
262
        }
263
    }
264
265 308
    /**
266
     * Binds a array of values to bound parameters.
267
     *
268
     * @param mixed[] $values
269
     *
270
     * @return bool
271
     */
272
    private function bindUntypedValues(array $values)
273
    {
274 630
        $params = [];
275
        $types  = str_repeat('s', count($values));
276 630
277 630
        foreach ($values as &$v) {
278
            $params[] =& $v;
279 630
        }
280 630
281
        return $this->_stmt->bind_param($types, ...$params);
282
    }
283 630
284
    /**
285
     * @return mixed[]|false|null
286
     */
287
    private function _fetch()
288
    {
289 1630
        $ret = $this->_stmt->fetch();
290
291 1630
        if ($ret === true) {
292
            $values = [];
293 1630
            foreach ($this->_rowBindedValues as $v) {
294 1546
                $values[] = $v;
295 1546
            }
296 1546
297
            return $values;
298
        }
299 1546
300
        return $ret;
301
    }
302 986
303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
307
    {
308 1693
        // do not try fetching from the statement if it's not expected to contain result
309
        // in order to prevent exceptional situation
310
        if (! $this->result) {
311
            return false;
312 1693
        }
313 63
314
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
315
316 1630
        if ($fetchMode === FetchMode::COLUMN) {
317
            return $this->fetchColumn();
318 1630
        }
319 7
320
        $values = $this->_fetch();
321
        if ($values === null) {
322 1630
            return false;
323 1630
        }
324 986
325
        if ($values === false) {
326
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
327 1546
        }
328
329
        if ($fetchMode === FetchMode::NUMERIC) {
330
            return $values;
331 1546
        }
332 420
333
        assert(is_array($this->_columnNames));
334
        $assoc = array_combine($this->_columnNames, $values);
335 1140
        assert(is_array($assoc));
336 1140
337 1140
        switch ($fetchMode) {
338
            case FetchMode::ASSOCIATIVE:
339 1140
                return $assoc;
340
341 1119
            case FetchMode::MIXED:
342
                return $assoc + $values;
343
344 14
            case FetchMode::STANDARD_OBJECT:
345
                return (object) $assoc;
346
347 7
            default:
348
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
349
        }
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
356
    {
357 881
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
358
359 881
        $rows = [];
360
361 881
        if ($fetchMode === FetchMode::COLUMN) {
362
            while (($row = $this->fetchColumn()) !== false) {
363 881
                $rows[] = $row;
364 63
            }
365 63
        } else {
366
            while (($row = $this->fetch($fetchMode)) !== false) {
367
                $rows[] = $row;
368 818
            }
369 762
        }
370
371
        return $rows;
372
    }
373 881
374
    /**
375
     * {@inheritdoc}
376
     */
377
    public function fetchColumn($columnIndex = 0)
378
    {
379 420
        $row = $this->fetch(FetchMode::NUMERIC);
380
381 420
        if ($row === false) {
382
            return false;
383 420
        }
384 105
385
        return $row[$columnIndex] ?? null;
386
    }
387 378
388
    /**
389
     * {@inheritdoc}
390
     */
391
    public function errorCode()
392
    {
393
        return $this->_stmt->errno;
394
    }
395
396
    /**
397
     * {@inheritdoc}
398
     */
399
    public function errorInfo()
400
    {
401
        return $this->_stmt->error;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_stmt->error returns the type string which is incompatible with the return type mandated by Doctrine\DBAL\Driver\Statement::errorInfo() of array<mixed,mixed>.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
402
    }
403
404
    /**
405
     * {@inheritdoc}
406
     */
407
    public function closeCursor()
408
    {
409 147
        $this->_stmt->free_result();
410
        $this->result = false;
411 147
412 147
        return true;
413
    }
414 147
415
    /**
416
     * {@inheritdoc}
417
     */
418
    public function rowCount()
419
    {
420 686
        if ($this->_columnNames === false) {
421
            return $this->_stmt->affected_rows;
422 686
        }
423 686
424
        return $this->_stmt->num_rows;
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430
    public function columnCount()
431
    {
432 28
        return $this->_stmt->field_count;
433
    }
434 28
435
    /**
436
     * {@inheritdoc}
437
     */
438
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
439
    {
440 1770
        $this->_defaultFetchMode = $fetchMode;
441
442 1770
        return true;
443
    }
444 1770
445
    /**
446
     * {@inheritdoc}
447
     */
448
    public function getIterator()
449
    {
450 57
        return new StatementIterator($this);
451
    }
452
}
453