Failed Conditions
Pull Request — master (#3217)
by Matthias
67:26 queued 02:55
created

MysqliStatement   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 447
Duplicated Lines 0 %

Test Coverage

Coverage 89.31%

Importance

Changes 0
Metric Value
wmc 63
eloc 163
dl 0
loc 447
ccs 117
cts 131
cp 0.8931
rs 3.36
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A bindValue() 0 17 3
A rowCount() 0 7 2
A _bindValues() 0 10 2
B fetch() 0 48 11
A bindParam() 0 16 3
A closeCursor() 0 6 1
A __construct() 0 12 3
A separateBoundValues() 0 27 6
A errorInfo() 0 3 1
A getIterator() 0 3 1
A errorCode() 0 3 1
C execute() 0 67 12
A _fetch() 0 14 3
A setFetchMode() 0 5 1
A columnCount() 0 3 1
A fetchColumn() 0 9 2
A sendLongData() 0 12 5
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
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Driver\Mysqli;
21
22
use Doctrine\DBAL\Driver\Statement;
23
use Doctrine\DBAL\Driver\StatementIterator;
24
use Doctrine\DBAL\Exception\InvalidArgumentException;
25
use Doctrine\DBAL\FetchMode;
26
use Doctrine\DBAL\ParameterType;
27
use function array_combine;
28
use function array_fill;
29
use function count;
30
use function feof;
31
use function fread;
32
use function get_resource_type;
33
use function is_resource;
34
use function str_repeat;
35
36
/**
37
 * @author Kim Hemsø Rasmussen <[email protected]>
38
 */
39
class MysqliStatement implements \IteratorAggregate, Statement
40
{
41
    /**
42
     * @var array
43
     */
44
    protected static $_paramTypeMap = [
45
        ParameterType::STRING       => 's',
46
        ParameterType::BINARY       => 's',
47
        ParameterType::BOOLEAN      => 'i',
48
        ParameterType::NULL         => 's',
49
        ParameterType::INTEGER      => 'i',
50
        ParameterType::LARGE_OBJECT => 'b',
51
    ];
52
53
    /**
54
     * @var \mysqli
55
     */
56
    protected $_conn;
57
58
    /**
59
     * @var \mysqli_stmt
60
     */
61
    protected $_stmt;
62
63
    /**
64
     * @var null|boolean|array
65
     */
66
    protected $_columnNames;
67
68
    /**
69
     * @var null|array
70
     */
71
    protected $_rowBindedValues;
72
73
    /**
74
     * @var array
75
     */
76
    protected $_bindedValues;
77
78
    /**
79
     * @var string
80
     */
81
    protected $types;
82
83
    /**
84
     * Contains ref values for bindValue().
85
     *
86
     * @var array
87
     */
88
    protected $_values = [];
89
90
    /**
91
     * @var int
92
     */
93
    protected $_defaultFetchMode = FetchMode::MIXED;
94
95
    /**
96
     * Indicates whether the statement is in the state when fetching results is possible
97
     *
98
     * @var bool
99
     */
100
    private $result = false;
101
102
    /**
103 765
     * @param \mysqli $conn
104
     * @param string  $prepareString
105 765
     *
106 765
     * @throws \Doctrine\DBAL\Driver\Mysqli\MysqliException
107 765
     */
108 12
    public function __construct(\mysqli $conn, $prepareString)
109
    {
110
        $this->_conn = $conn;
111 753
        $this->_stmt = $conn->prepare($prepareString);
112 753
        if (false === $this->_stmt) {
0 ignored issues
show
introduced by
The condition $this->_stmt is always false. If $this->_stmt can have other possible types, add them to lib/Doctrine/DBAL/Driver...qli/MysqliStatement.php:59
Loading history...
113 366
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
114 366
        }
115
116 753
        $paramCount = $this->_stmt->param_count;
117
        if (0 < $paramCount) {
118
            $this->types = str_repeat('s', $paramCount);
119
            $this->_bindedValues = array_fill(1, $paramCount, null);
120
        }
121 21
    }
122
123 21
    /**
124
     * {@inheritdoc}
125
     */
126 21
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
127 21
    {
128
        if (null === $type) {
129
            $type = 's';
130
        } else {
131
            if (isset(self::$_paramTypeMap[$type])) {
132
                $type = self::$_paramTypeMap[$type];
133 21
            } else {
134 21
                throw new MysqliException("Unknown type: '{$type}'");
135
            }
136 21
        }
137
138
        $this->_bindedValues[$column] =& $variable;
139
        $this->types[$column - 1] = $type;
140
141
        return true;
142 102
    }
143
144 102
    /**
145
     * {@inheritdoc}
146
     */
147 102
    public function bindValue($param, $value, $type = ParameterType::STRING)
148 102
    {
149
        if (null === $type) {
150
            $type = 's';
151
        } else {
152
            if (isset(self::$_paramTypeMap[$type])) {
153
                $type = self::$_paramTypeMap[$type];
154 102
            } else {
155 102
                throw new MysqliException("Unknown type: '{$type}'");
156 102
            }
157
        }
158 102
159
        $this->_values[$param] = $value;
160
        $this->_bindedValues[$param] =& $this->_values[$param];
161
        $this->types[$param - 1] = $type;
162
163
        return true;
164 729
    }
165
166 729
    /**
167 366
     * {@inheritdoc}
168 264
     */
169 264
    public function execute($params = null)
170
    {
171
        if (null !== $this->_bindedValues) {
172 123
            if (null !== $params) {
173
                if ( ! $this->_bindValues($params)) {
174
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
175
                }
176
            } else {
177
                list($types, $values, $streams) = $this->separateBoundValues();
178 729
                if (! $this->_stmt->bind_param($types, ...$values)) {
0 ignored issues
show
Bug introduced by
$types of type array<mixed,mixed> is incompatible with the type string expected by parameter $types of mysqli_stmt::bind_param(). ( Ignorable by Annotation )

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

178
                if (! $this->_stmt->bind_param(/** @scrutinizer ignore-type */ $types, ...$values)) {
Loading history...
179 18
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
180
                }
181
                $this->sendLongData($streams);
182 726
            }
183 726
        }
184 726
185 669
        if ( ! $this->_stmt->execute()) {
186 669
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
187 669
        }
188
189 669
        if (null === $this->_columnNames) {
190
            $meta = $this->_stmt->result_metadata();
191 669
            if (false !== $meta) {
192
                $columnNames = [];
193 286
                foreach ($meta->fetch_fields() as $col) {
194
                    $columnNames[] = $col->name;
195
                }
196
                $meta->free();
197 726
198
                $this->_columnNames = $columnNames;
199
            } else {
200
                $this->_columnNames = false;
201 669
            }
202
        }
203
204
        if (false !== $this->_columnNames) {
205
            // Store result of every execution which has it. Otherwise it will be impossible
206
            // to execute a new statement in case if the previous one has non-fetched rows
207
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
208
            $this->_stmt->store_result();
209
210
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
211
            // it will have to allocate as much memory as it may be needed for the given column type
212
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
213
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
214 669
            //
215
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
216 669
            // previously called on the statement, the values are unbound making the statement unusable.
217 669
            //
218 669
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
219
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
220
            // to the length of the ones fetched during the previous execution.
221 669
            $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

221
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
222
223
            $refs = [];
224
            foreach ($this->_rowBindedValues as $key => &$value) {
225
                $refs[$key] =& $value;
226 726
            }
227
228 726
            if (! $this->_stmt->bind_result(...$refs)) {
229
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
230
            }
231
        }
232
233
        $this->result = true;
234
235
        return true;
236
    }
237
238 264
    /**
239
     * Split $this->_bindedValues into those values that need to be sent using mysqli::send_long_data()
240 264
     * and those that can be bound the usual way.
241 264
     *
242
     * @return mixed[][] [0] => the type mapping, [1] => the regular values, [2] => streams
243 264
     */
244 264
    private function separateBoundValues()
245
    {
246
        $streams = $values = [];
247 264
        $types   = $this->types;
248
249
        foreach ($this->_bindedValues as $parameter => $value) {
250
            if (! isset($types[$parameter - 1])) {
251
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
252
            }
253 645
254
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
255 645
                if (is_resource($value)) {
256
                    if (get_resource_type($value) !== 'stream') {
257 645
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
258 612
                    }
259 612
                    $streams[$parameter] = $value;
260 612
                    $values[$parameter]  = null;
261
                    continue;
262
                } else {
263 612
                    $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
264
                }
265
            }
266 372
267
            $values[$parameter] = $value;
268
        }
269
270
        return [$types, $values, $streams];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($types, $values, $streams) returns an array which contains values of type string which are incompatible with the documented value type array<mixed,mixed>.
Loading history...
271
    }
272 672
273
    /**
274
     * Handle $this->_longData after regular query parameters have been bound
275
     *
276 672
     * @throws MysqliException
277 27
     */
278
    private function sendLongData($streams)
279
    {
280 645
        foreach ($streams as $paramNr => $stream) {
281
            while (! feof($stream)) {
282 645
                $chunk = fread($stream, 8192);
283 3
284
                if ($chunk === false) {
285
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
286 645
                }
287 645
288 372
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
289
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
290
                }
291 612
            }
292
        }
293
    }
294
295
    /**
296 612
     * Binds a array of values to bound parameters.
297 162
     *
298
     * @param array $values
299 456
     *
300 447
     * @return bool
301
     */
302 9
    private function _bindValues($values)
303 6
    {
304 6
        $params = [];
305
        $types = str_repeat('s', count($values));
306 6
307
        foreach ($values as &$v) {
308 3
            $params[] =& $v;
309 3
        }
310 3
311
        return $this->_stmt->bind_param($types, ...$params);
312 3
    }
313 3
314
    /**
315
     * @return mixed[]|false
316 3
     */
317
    private function _fetch()
318
    {
319
        $ret = $this->_stmt->fetch();
320
321
        if (true === $ret) {
322
            $values = [];
323
            foreach ($this->_rowBindedValues as $v) {
324
                $values[] = $v;
325
            }
326 327
327
            return $values;
328 327
        }
329
330 327
        return $ret;
331
    }
332 327
333 12
    /**
334 12
     * {@inheritdoc}
335
     */
336
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
337 315
    {
338 294
        // do not try fetching from the statement if it's not expected to contain result
339
        // in order to prevent exceptional situation
340
        if (!$this->result) {
341
            return false;
342 327
        }
343
344
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
345
346
        if ($fetchMode === FetchMode::COLUMN) {
347
            return $this->fetchColumn();
348 162
        }
349
350 162
        $values = $this->_fetch();
351
        if (null === $values) {
0 ignored issues
show
introduced by
The condition null === $values is always false.
Loading history...
352 162
            return false;
353 30
        }
354
355
        if (false === $values) {
356 144
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
357
        }
358
359
        switch ($fetchMode) {
360
            case FetchMode::NUMERIC:
361
                return $values;
362
363
            case FetchMode::ASSOCIATIVE:
364
                return array_combine($this->_columnNames, $values);
0 ignored issues
show
Bug introduced by
It seems like $this->_columnNames can also be of type null and boolean; however, parameter $keys of array_combine() 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

364
                return array_combine(/** @scrutinizer ignore-type */ $this->_columnNames, $values);
Loading history...
365
366
            case FetchMode::MIXED:
367
                $ret = array_combine($this->_columnNames, $values);
368
                $ret += $values;
369
370
                return $ret;
371
372
            case FetchMode::STANDARD_OBJECT:
373
                $assoc = array_combine($this->_columnNames, $values);
374
                $ret = new \stdClass();
375
376
                foreach ($assoc as $column => $value) {
377
                    $ret->$column = $value;
378 60
                }
379
380 60
                return $ret;
381 60
382
            default:
383 60
                throw new MysqliException("Unknown fetch type '{$fetchMode}'");
384
        }
385
    }
386
387
    /**
388
     * {@inheritdoc}
389 279
     */
390
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
391 279
    {
392 279
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
393
394
        $rows = [];
395
396
        if ($fetchMode === FetchMode::COLUMN) {
397
            while (($row = $this->fetchColumn()) !== false) {
398
                $rows[] = $row;
399
            }
400
        } else {
401 12
            while (($row = $this->fetch($fetchMode)) !== false) {
402
                $rows[] = $row;
403 12
            }
404
        }
405
406
        return $rows;
407
    }
408
409 705
    /**
410
     * {@inheritdoc}
411 705
     */
412
    public function fetchColumn($columnIndex = 0)
413 705
    {
414
        $row = $this->fetch(FetchMode::NUMERIC);
415
416
        if (false === $row) {
417
            return false;
418
        }
419 41
420
        return $row[$columnIndex] ?? null;
421 41
    }
422
423
    /**
424
     * {@inheritdoc}
425
     */
426
    public function errorCode()
427
    {
428
        return $this->_stmt->errno;
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     */
434
    public function errorInfo()
435
    {
436
        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.

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...
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     */
442
    public function closeCursor()
443
    {
444
        $this->_stmt->free_result();
445
        $this->result = false;
446
447
        return true;
448
    }
449
450
    /**
451
     * {@inheritdoc}
452
     */
453
    public function rowCount()
454
    {
455
        if (false === $this->_columnNames) {
456
            return $this->_stmt->affected_rows;
457
        }
458
459
        return $this->_stmt->num_rows;
460
    }
461
462
    /**
463
     * {@inheritdoc}
464
     */
465
    public function columnCount()
466
    {
467
        return $this->_stmt->field_count;
468
    }
469
470
    /**
471
     * {@inheritdoc}
472
     */
473
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
474
    {
475
        $this->_defaultFetchMode = $fetchMode;
476
477
        return true;
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483
    public function getIterator()
484
    {
485
        return new StatementIterator($this);
486
    }
487
}
488