Failed Conditions
Pull Request — 2.11.x (#3891)
by Malte
62:14
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
        foreach (SQLParserUtils::getPlaceholderPositions($query, false) as $placeholderPosition => $placeholderName) {
123
            $placeholderName = (string) $placeholderName;
124 1805
            if (array_key_exists($placeholderName, $this->placeholderNamesToNumbers) === false) {
125
                $this->placeholderNamesToNumbers[$placeholderName] = [];
126
            }
127
128 1805
            $this->placeholderNamesToNumbers[$placeholderName][] = $placeholderNumber++;
129 1805
130 1805
            $placeholderPositionInShortenedQuery = $placeholderPosition - $numberOfCharsQueryIsShortenedBy;
131
            $placeholderNameLength               = strlen($placeholderName);
132 1805
            $query                               = substr($query, 0, $placeholderPositionInShortenedQuery) . '?' . substr($query, ($placeholderPositionInShortenedQuery + $placeholderNameLength + 1));
133
            $numberOfCharsQueryIsShortenedBy    += $placeholderNameLength;
134
        }
135
136
        return $query;
137
    }
138 1805
139
    /**
140 1805
     * {@inheritdoc}
141 1805
     */
142 1742
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
143 1742
    {
144
        assert(is_int($column));
145
146 1805
        if (! isset(self::$_paramTypeMap[$type])) {
147
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
148
        }
149
150 1805
        $this->_bindedValues[$column] =& $variable;
151 1385
        $this->types[$column - 1]     = self::$_paramTypeMap[$type];
152
153
        return true;
154 1805
    }
155 1805
156 1805
    /**
157 1798
     * {@inheritdoc}
158 1798
     */
159
    public function bindValue($param, $value, $type = ParameterType::STRING)
160 1798
    {
161 1798
        assert(is_int($param));
162 1798
163
        if (! isset(self::$_paramTypeMap[$type])) {
164
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
165 1798
        }
166
167 1798
        $this->_values[$param]       = $value;
168
        $this->_bindedValues[$param] =& $this->_values[$param];
169 1805
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
170
171
        return true;
172
    }
173 1805
174
    /**
175
     * {@inheritdoc}
176
     */
177 1798
    public function execute($params = null)
178
    {
179
        if (is_array($params) && count($params) > 0) {
180
            $params = $this->convertNamedToPositionalParams($params);
181
        }
182
183
        if ($this->_bindedValues !== null) {
184
            if ($params !== null) {
185
                if (! $this->bindUntypedValues($params)) {
186
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
187
                }
188
            } else {
189
                $this->bindTypedParameters();
190 1798
            }
191
        }
192 1798
193 1798
        if (! $this->_stmt->execute()) {
194 1798
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
195
        }
196
197 1798
        if ($this->_columnNames === null) {
198
            $meta = $this->_stmt->result_metadata();
199
            if ($meta !== false) {
200
                $fields = $meta->fetch_fields();
201
                assert(is_array($fields));
202 1805
203
                $columnNames = [];
204 1805
                foreach ($fields as $col) {
205
                    $columnNames[] = $col->name;
206
                }
207
208
                $meta->free();
209
210 1805
                $this->_columnNames = $columnNames;
211
            } else {
212 1805
                $this->_columnNames = false;
213 1805
            }
214
        }
215 1805
216 1805
        if ($this->_columnNames !== false) {
217
            // Store result of every execution which has it. Otherwise it will be impossible
218
            // to execute a new statement in case if the previous one has non-fetched rows
219
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
220 1805
            $this->_stmt->store_result();
221 1805
222 1798
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
223
            // it will have to allocate as much memory as it may be needed for the given column type
224
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
225 1798
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
226 1798
            //
227 1798
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
228
            // previously called on the statement, the values are unbound making the statement unusable.
229
            //
230 1805
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
231
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
232
            // to the length of the ones fetched during the previous execution.
233 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

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