Passed
Push — type-registry ( f9a1df...0931f1 )
by Michael
24:09
created

MysqliStatement   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 425
Duplicated Lines 0 %

Test Coverage

Coverage 90.13%

Importance

Changes 0
Metric Value
wmc 60
eloc 154
dl 0
loc 425
ccs 137
cts 152
cp 0.9013
rs 3.6
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A rowCount() 0 7 2
A columnCount() 0 3 1
A bindValue() 0 13 2
B fetch() 0 44 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 _fetch() 0 14 3
A setFetchMode() 0 5 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[] */
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
     */
79 1812
    public function __construct(mysqli $conn, $prepareString)
80
    {
81 1812
        $this->_conn = $conn;
82
83 1812
        $stmt = $conn->prepare($prepareString);
84
85 1812
        if ($stmt === false) {
86 1301
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
87
        }
88
89 1812
        $this->_stmt = $stmt;
90
91 1812
        $paramCount = $this->_stmt->param_count;
92 1812
        if (0 >= $paramCount) {
93 1805
            return;
94
        }
95
96 1812
        $this->types         = str_repeat('s', $paramCount);
97 1812
        $this->_bindedValues = array_fill(1, $paramCount, null);
98 1812
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103 1777
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
104
    {
105 1777
        assert(is_int($column));
106
107 1777
        if (! isset(self::$_paramTypeMap[$type])) {
108
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
109
        }
110
111 1777
        $this->_bindedValues[$column] =& $variable;
112 1777
        $this->types[$column - 1]     = self::$_paramTypeMap[$type];
113
114 1777
        return true;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 1812
    public function bindValue($param, $value, $type = ParameterType::STRING)
121
    {
122 1812
        assert(is_int($param));
123
124 1812
        if (! isset(self::$_paramTypeMap[$type])) {
125
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
126
        }
127
128 1812
        $this->_values[$param]       = $value;
129 1812
        $this->_bindedValues[$param] =& $this->_values[$param];
130 1812
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
131
132 1812
        return true;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 1812
    public function execute($params = null)
139
    {
140 1812
        if ($this->_bindedValues !== null) {
141 1812
            if ($params !== null) {
142 1749
                if (! $this->bindUntypedValues($params)) {
143 1749
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
144
                }
145
            } else {
146 1812
                $this->bindTypedParameters();
147
            }
148
        }
149
150 1812
        if (! $this->_stmt->execute()) {
151 1308
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
152
        }
153
154 1812
        if ($this->_columnNames === null) {
155 1812
            $meta = $this->_stmt->result_metadata();
156 1812
            if ($meta !== false) {
157 1805
                $fields = $meta->fetch_fields();
158 1805
                assert(is_array($fields));
159
160 1805
                $columnNames = [];
161 1805
                foreach ($fields as $col) {
162 1805
                    $columnNames[] = $col->name;
163
                }
164
165 1805
                $meta->free();
166
167 1805
                $this->_columnNames = $columnNames;
168
            } else {
169 1812
                $this->_columnNames = false;
170
            }
171
        }
172
173 1812
        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
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
177 1805
            $this->_stmt->store_result();
178
179
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
180
            // 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 1805
            $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 1805
            $refs = [];
193 1805
            foreach ($this->_rowBindedValues as $key => &$value) {
194 1805
                $refs[$key] =& $value;
195
            }
196
197 1805
            if (! $this->_stmt->bind_result(...$refs)) {
198
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
199
            }
200
        }
201
202 1812
        $this->result = true;
203
204 1812
        return true;
205
    }
206
207
    /**
208
     * Binds parameters with known types previously bound to the statement
209
     */
210 1812
    private function bindTypedParameters()
211
    {
212 1812
        $streams = $values = [];
213 1812
        $types   = $this->types;
214
215 1812
        foreach ($this->_bindedValues as $parameter => $value) {
216 1812
            if (! isset($types[$parameter - 1])) {
217
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
218
            }
219
220 1812
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
221 1812
                if (is_resource($value)) {
222 1805
                    if (get_resource_type($value) !== 'stream') {
223
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
224
                    }
225 1805
                    $streams[$parameter] = $value;
226 1805
                    $values[$parameter]  = null;
227 1805
                    continue;
228
                }
229
230 1812
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
231
            }
232
233 1812
            $values[$parameter] = $value;
234
        }
235
236 1812
        if (! $this->_stmt->bind_param($types, ...$values)) {
237
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
238
        }
239
240 1812
        $this->sendLongData($streams);
241 1812
    }
242
243
    /**
244
     * Handle $this->_longData after regular query parameters have been bound
245
     *
246
     * @throws MysqliException
247
     */
248 1812
    private function sendLongData($streams)
249
    {
250 1812
        foreach ($streams as $paramNr => $stream) {
251 1805
            while (! feof($stream)) {
252 1805
                $chunk = fread($stream, 8192);
253
254 1805
                if ($chunk === false) {
255
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
256
                }
257
258 1805
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
259
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
260
                }
261
            }
262
        }
263 1812
    }
264
265
    /**
266
     * Binds a array of values to bound parameters.
267
     *
268
     * @param mixed[] $values
269
     *
270
     * @return bool
271
     */
272 1749
    private function bindUntypedValues(array $values)
273
    {
274 1749
        $params = [];
275 1749
        $types  = str_repeat('s', count($values));
276
277 1749
        foreach ($values as &$v) {
278 1749
            $params[] =& $v;
279
        }
280
281 1749
        return $this->_stmt->bind_param($types, ...$params);
282
    }
283
284
    /**
285
     * @return mixed[]|false|null
286
     */
287 1805
    private function _fetch()
288
    {
289 1805
        $ret = $this->_stmt->fetch();
290
291 1805
        if ($ret === true) {
292 1805
            $values = [];
293 1805
            foreach ($this->_rowBindedValues as $v) {
294 1805
                $values[] = $v;
295
            }
296
297 1805
            return $values;
298
        }
299
300 1805
        return $ret;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 1805
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
307
    {
308
        // do not try fetching from the statement if it's not expected to contain result
309
        // in order to prevent exceptional situation
310 1805
        if (! $this->result) {
311 336
            return false;
312
        }
313
314 1805
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
315
316 1805
        if ($fetchMode === FetchMode::COLUMN) {
317 259
            return $this->fetchColumn();
318
        }
319
320 1805
        $values = $this->_fetch();
321
322 1805
        if ($values === null) {
323 1805
            return false;
324
        }
325
326 1805
        if ($values === false) {
327
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
328
        }
329
330 1805
        if ($fetchMode === FetchMode::NUMERIC) {
331 1805
            return $values;
332
        }
333
334 1749
        assert(is_array($this->_columnNames));
335 1749
        $assoc = array_combine($this->_columnNames, $values);
336 1749
        assert(is_array($assoc));
337
338 1749
        switch ($fetchMode) {
339
            case FetchMode::ASSOCIATIVE:
340 1749
                return $assoc;
341
342
            case FetchMode::MIXED:
343 1728
                return $assoc + $values;
344
345
            case FetchMode::STANDARD_OBJECT:
346 1364
                return (object) $assoc;
347
348
            default:
349
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
350
        }
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356 1805
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
357
    {
358 1805
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
359
360 1805
        $rows = [];
361
362 1805
        if ($fetchMode === FetchMode::COLUMN) {
363 1805
            while (($row = $this->fetchColumn()) !== false) {
364 1805
                $rows[] = $row;
365
            }
366
        } else {
367 1735
            while (($row = $this->fetch($fetchMode)) !== false) {
368 1735
                $rows[] = $row;
369
            }
370
        }
371
372 1805
        return $rows;
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 1805
    public function fetchColumn($columnIndex = 0)
379
    {
380 1805
        $row = $this->fetch(FetchMode::NUMERIC);
381
382 1805
        if ($row === false) {
383 1805
            return false;
384
        }
385
386 1805
        return $row[$columnIndex] ?? null;
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392
    public function errorCode()
393
    {
394
        return $this->_stmt->errno;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function errorInfo()
401
    {
402
        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...
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     */
408 984
    public function closeCursor()
409
    {
410 984
        $this->_stmt->free_result();
411 984
        $this->result = false;
412
413 984
        return true;
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     */
419 1812
    public function rowCount()
420
    {
421 1812
        if ($this->_columnNames === false) {
422 1812
            return $this->_stmt->affected_rows;
423
        }
424
425
        return $this->_stmt->num_rows;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 984
    public function columnCount()
432
    {
433 984
        return $this->_stmt->field_count;
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439 1805
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
440
    {
441 1805
        $this->_defaultFetchMode = $fetchMode;
442
443 1805
        return true;
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449 1862
    public function getIterator()
450
    {
451 1862
        return new StatementIterator($this);
452
    }
453
}
454