Failed Conditions
Pull Request — 2.11.x (#3891)
by Malte
61:34
created

MysqliStatement::execute()   C

Complexity

Conditions 13
Paths 62

Size

Total Lines 71
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 13.4931

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 32
c 1
b 0
f 0
dl 0
loc 71
ccs 24
cts 28
cp 0.8571
rs 6.6166
cc 13
nc 62
nop 1
crap 13.4931

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Doctrine\DBAL\SQLParserUtils;
11
use IteratorAggregate;
12
use mysqli;
13
use mysqli_stmt;
14
use PDO;
15
use function array_combine;
16
use function array_fill;
17
use function array_key_exists;
18
use function array_keys;
19
use function assert;
20
use function count;
21
use function feof;
22
use function fread;
23
use function get_resource_type;
24
use function is_array;
25
use function is_int;
26
use function is_resource;
27
use function is_string;
28
use function sprintf;
29
use function str_repeat;
30
use function strlen;
31
use function substr;
32
33
class MysqliStatement implements IteratorAggregate, Statement
34
{
35
    /** @var string[] */
36
    protected static $_paramTypeMap = [
37
        ParameterType::STRING       => 's',
38
        ParameterType::BINARY       => 's',
39
        ParameterType::BOOLEAN      => 'i',
40
        ParameterType::NULL         => 's',
41
        ParameterType::INTEGER      => 'i',
42
        ParameterType::LARGE_OBJECT => 'b',
43
    ];
44
45
    /** @var mysqli */
46
    protected $_conn;
47
48
    /** @var mysqli_stmt */
49
    protected $_stmt;
50
51
    /** @var string[]|false|null */
52
    protected $_columnNames;
53
54
    /** @var mixed[] */
55
    protected $_rowBindedValues = [];
56
57
    /** @var mixed[] */
58
    protected $_bindedValues;
59
60
    /** @var string */
61
    protected $types;
62
63
    /** @var array<string, array<int, int>> maps parameter names to their placeholder number(s). */
64
    protected $placeholderNamesToNumbers = [];
65
66
    /**
67
     * Contains ref values for bindValue().
68
     *
69
     * @var mixed[]
70
     */
71
    protected $_values = [];
72
73
    /** @var int */
74
    protected $_defaultFetchMode = FetchMode::MIXED;
75
76
    /**
77
     * Indicates whether the statement is in the state when fetching results is possible
78
     *
79 1805
     * @var bool
80
     */
81 1805
    private $result = false;
82
83 1805
    /**
84
     * @param string $prepareString
85 1805
     *
86 1378
     * @throws MysqliException
87
     */
88
    public function __construct(mysqli $conn, $prepareString)
89 1805
    {
90
        $this->_conn = $conn;
91 1805
92 1805
        $queryWithoutNamedParameters = $this->convertNamedToPositionalPlaceholders($prepareString);
93 1798
        $stmt                        = $conn->prepare($queryWithoutNamedParameters);
94
95
        if ($stmt === false) {
96 1805
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
97 1805
        }
98 1805
99
        $this->_stmt = $stmt;
100
101
        $paramCount = $this->_stmt->param_count;
102
        if (0 >= $paramCount) {
103 1770
            return;
104
        }
105 1770
106
        $this->types         = str_repeat('s', $paramCount);
107 1770
        $this->_bindedValues = array_fill(1, $paramCount, null);
108
    }
109
110
    /**
111 1770
     * Converts named placeholders (":parameter") into positional ones ("?"), as MySQL does not support them.
112 1770
     *
113
     * @param string $query The query string to create a prepared statement of.
114 1770
     *
115
     * @return string
116
     */
117
    private function convertNamedToPositionalPlaceholders($query)
118
    {
119
        $numberOfCharsQueryIsShortenedBy = 0;
120 1805
        $placeholderNumber               = 0;
121
122 1805
        /** @var string[] $placeholderPositions */
123
        $placeholderPositions = SQLParserUtils::getPlaceholderPositions($query, false);
124 1805
        foreach ($placeholderPositions as $placeholderPosition => $placeholderName) {
125
            if (array_key_exists($placeholderName, $this->placeholderNamesToNumbers) === false) {
126
                $this->placeholderNamesToNumbers[$placeholderName] = [];
127
            }
128 1805
129 1805
            $this->placeholderNamesToNumbers[$placeholderName][] = $placeholderNumber++;
130 1805
131
            $placeholderPositionInShortenedQuery = $placeholderPosition - $numberOfCharsQueryIsShortenedBy;
132 1805
            $placeholderNameLength               = strlen($placeholderName);
133
            $query                               = substr($query, 0, $placeholderPositionInShortenedQuery) . '?' . substr($query, ($placeholderPositionInShortenedQuery + $placeholderNameLength + 1));
134
            $numberOfCharsQueryIsShortenedBy    += $placeholderNameLength;
135
        }
136
137
        return $query;
138 1805
    }
139
140 1805
    /**
141 1805
     * {@inheritdoc}
142 1742
     */
143 1742
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
144
    {
145
        assert(is_int($column));
146 1805
147
        if (! isset(self::$_paramTypeMap[$type])) {
148
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
149
        }
150 1805
151 1385
        $this->_bindedValues[$column] =& $variable;
152
        $this->types[$column - 1]     = self::$_paramTypeMap[$type];
153
154 1805
        return true;
155 1805
    }
156 1805
157 1798
    /**
158 1798
     * {@inheritdoc}
159
     */
160 1798
    public function bindValue($param, $value, $type = ParameterType::STRING)
161 1798
    {
162 1798
        assert(is_int($param));
163
164
        if (! isset(self::$_paramTypeMap[$type])) {
165 1798
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
166
        }
167 1798
168
        $this->_values[$param]       = $value;
169 1805
        $this->_bindedValues[$param] =& $this->_values[$param];
170
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
171
172
        return true;
173 1805
    }
174
175
    /**
176
     * {@inheritdoc}
177 1798
     */
178
    public function execute($params = null)
179
    {
180
        if (is_array($params) && count($params) > 0) {
181
            $params = $this->convertNamedToPositionalParams($params);
182
        }
183
184
        if ($this->_bindedValues !== null) {
185
            if ($params !== null) {
186
                if (! $this->bindUntypedValues($params)) {
187
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
188
                }
189
            } else {
190 1798
                $this->bindTypedParameters();
191
            }
192 1798
        }
193 1798
194 1798
        if (! $this->_stmt->execute()) {
195
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
196
        }
197 1798
198
        if ($this->_columnNames === null) {
199
            $meta = $this->_stmt->result_metadata();
200
            if ($meta !== false) {
201
                $fields = $meta->fetch_fields();
202 1805
                assert(is_array($fields));
203
204 1805
                $columnNames = [];
205
                foreach ($fields as $col) {
206
                    $columnNames[] = $col->name;
207
                }
208
209
                $meta->free();
210 1805
211
                $this->_columnNames = $columnNames;
212 1805
            } else {
213 1805
                $this->_columnNames = false;
214
            }
215 1805
        }
216 1805
217
        if ($this->_columnNames !== false) {
218
            // Store result of every execution which has it. Otherwise it will be impossible
219
            // to execute a new statement in case if the previous one has non-fetched rows
220 1805
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
221 1805
            $this->_stmt->store_result();
222 1798
223
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
224
            // it will have to allocate as much memory as it may be needed for the given column type
225 1798
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
226 1798
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
227 1798
            //
228
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
229
            // previously called on the statement, the values are unbound making the statement unusable.
230 1805
            //
231
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
232
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
233 1805
            // to the length of the ones fetched during the previous execution.
234
            $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

234
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
235
236 1805
            $refs = [];
237
            foreach ($this->_rowBindedValues as $key => &$value) {
238
                $refs[$key] =& $value;
239
            }
240 1805
241 1805
            if (! $this->_stmt->bind_result(...$refs)) {
242
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
243
            }
244
        }
245
246
        $this->result = true;
247
248 1805
        return true;
249
    }
250 1805
251 1798
    /**
252 1798
     * Converts an array of named parameters, e.g. ['id' => 1, 'foo' => 'bar'] to the corresponding array with
253
     * positional parameters referring to the prepared query, e.g. [1 => 1, 2 => 'bar', 3 => 'bar'] for a prepared query
254 1798
     * like "SELECT id FROM table WHERE foo = :foo and baz = :foo".
255
     *
256
     * @param mixed[] $params
257
     *
258 1798
     * @return mixed[]
259
     */
260
    private function convertNamedToPositionalParams(array $params)
261
    {
262
        $namedParamsAreUsed = is_string(array_keys($params)[0]);
263 1805
        if ($namedParamsAreUsed === false) {
264
            return $params;
265
        }
266
267
        $positionalParameters = [];
268
269
        foreach ($params as $paramName => $paramValue) {
270
            foreach ($this->placeholderNamesToNumbers[$paramName] as $number) {
271
                $positionalParameters[$number] = $paramValue;
272 1742
            }
273
        }
274 1742
275 1742
        return $positionalParameters;
276
    }
277 1742
278 1742
    /**
279
     * Binds parameters with known types previously bound to the statement
280
     */
281 1742
    private function bindTypedParameters()
282
    {
283
        $streams = $values = [];
284
        $types   = $this->types;
285
286
        foreach ($this->_bindedValues as $parameter => $value) {
287 1798
            if (! isset($types[$parameter - 1])) {
288
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
289 1798
            }
290
291 1798
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
292 1798
                if (is_resource($value)) {
293 1798
                    if (get_resource_type($value) !== 'stream') {
294 1798
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
295
                    }
296
                    $streams[$parameter] = $value;
297 1798
                    $values[$parameter]  = null;
298
                    continue;
299
                }
300 1798
301
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
302
            }
303
304
            $values[$parameter] = $value;
305
        }
306 1798
307
        if (! $this->_stmt->bind_param($types, ...$values)) {
308
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
309
        }
310 1798
311 348
        $this->sendLongData($streams);
312
    }
313
314 1798
    /**
315
     * Handle $this->_longData after regular query parameters have been bound
316 1798
     *
317 271
     * @throws MysqliException
318
     */
319
    private function sendLongData($streams)
320 1798
    {
321
        foreach ($streams as $paramNr => $stream) {
322 1798
            while (! feof($stream)) {
323 1798
                $chunk = fread($stream, 8192);
324
325
                if ($chunk === false) {
326 1798
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
327
                }
328
329
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
330 1798
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
331 1798
                }
332
            }
333
        }
334 1742
    }
335 1742
336 1742
    /**
337
     * Binds a array of values to bound parameters.
338 1742
     *
339
     * @param mixed[] $values
340 1742
     *
341
     * @return bool
342
     */
343 1721
    private function bindUntypedValues(array $values)
344
    {
345
        $params = [];
346 1441
        $types  = str_repeat('s', count($values));
347
348
        foreach ($values as &$v) {
349
            $params[] =& $v;
350
        }
351
352
        return $this->_stmt->bind_param($types, ...$params);
353
    }
354
355
    /**
356 1798
     * @return mixed[]|false|null
357
     */
358 1798
    private function _fetch()
359
    {
360 1798
        $ret = $this->_stmt->fetch();
361
362 1798
        if ($ret === true) {
363 1798
            $values = [];
364 1798
            foreach ($this->_rowBindedValues as $v) {
365
                $values[] = $v;
366
            }
367 1728
368 1728
            return $values;
369
        }
370
371
        return $ret;
372 1798
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
378 1798
    {
379
        // do not try fetching from the statement if it's not expected to contain result
380 1798
        // in order to prevent exceptional situation
381
        if (! $this->result) {
382 1798
            return false;
383 1798
        }
384
385
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
386 1798
387
        if ($fetchMode === FetchMode::COLUMN) {
388
            return $this->fetchColumn();
389
        }
390
391
        $values = $this->_fetch();
392
393
        if ($values === null) {
394
            return false;
395
        }
396
397
        if ($values === false) {
398
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
399
        }
400
401
        if ($fetchMode === FetchMode::NUMERIC) {
402
            return $values;
403
        }
404
405
        assert(is_array($this->_columnNames));
406
        $assoc = array_combine($this->_columnNames, $values);
407
        assert(is_array($assoc));
408 1053
409
        switch ($fetchMode) {
410 1053
            case FetchMode::ASSOCIATIVE:
411 1053
                return $assoc;
412
413 1053
            case FetchMode::MIXED:
414
                return $assoc + $values;
415
416
            case FetchMode::STANDARD_OBJECT:
417
                return (object) $assoc;
418
419 1805
            default:
420
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
421 1805
        }
422 1805
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
428
    {
429
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
430
431 1053
        $rows = [];
432
433 1053
        if ($fetchMode === FetchMode::COLUMN) {
434
            while (($row = $this->fetchColumn()) !== false) {
435
                $rows[] = $row;
436
            }
437
        } else {
438
            while (($row = $this->fetch($fetchMode)) !== false) {
439 1798
                $rows[] = $row;
440
            }
441 1798
        }
442
443 1798
        return $rows;
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449 1858
    public function fetchColumn($columnIndex = 0)
450
    {
451 1858
        $row = $this->fetch(FetchMode::NUMERIC);
452
453
        if ($row === false) {
454
            return false;
455
        }
456
457
        return $row[$columnIndex] ?? null;
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463
    public function errorCode()
464
    {
465
        return $this->_stmt->errno;
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     */
471
    public function errorInfo()
472
    {
473
        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...
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479
    public function closeCursor()
480
    {
481
        $this->_stmt->free_result();
482
        $this->result = false;
483
484
        return true;
485
    }
486
487
    /**
488
     * {@inheritdoc}
489
     */
490
    public function rowCount()
491
    {
492
        if ($this->_columnNames === false) {
493
            return $this->_stmt->affected_rows;
494
        }
495
496
        return $this->_stmt->num_rows;
497
    }
498
499
    /**
500
     * {@inheritdoc}
501
     */
502
    public function columnCount()
503
    {
504
        return $this->_stmt->field_count;
505
    }
506
507
    /**
508
     * {@inheritdoc}
509
     */
510
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
511
    {
512
        $this->_defaultFetchMode = $fetchMode;
513
514
        return true;
515
    }
516
517
    /**
518
     * {@inheritdoc}
519
     */
520
    public function getIterator()
521
    {
522
        return new StatementIterator($this);
523
    }
524
}
525