Completed
Pull Request — 2.10.x (#4009)
by Grégoire
08:50
created

MysqliStatement::execute()   C

Complexity

Conditions 11
Paths 31

Size

Total Lines 68

Duplication

Lines 6
Ratio 8.82 %

Code Coverage

Tests 28
CRAP Score 11.0048

Importance

Changes 0
Metric Value
dl 6
loc 68
ccs 28
cts 29
cp 0.9655
rs 6.5515
c 0
b 0
f 0
cc 11
nc 31
nop 1
crap 11.0048

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 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 1543
    public function __construct(mysqli $conn, $prepareString)
80
    {
81 1543
        $this->_conn = $conn;
82
83 1543
        $stmt = $conn->prepare($prepareString);
84
85 1543
        if ($stmt === false) {
86 1177
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
87
        }
88
89 1543
        $this->_stmt = $stmt;
90
91 1543
        $paramCount = $this->_stmt->param_count;
92 1543
        if (0 >= $paramCount) {
93 1537
            return;
94
        }
95
96 1543
        $this->types         = str_repeat('s', $paramCount);
97 1543
        $this->_bindedValues = array_fill(1, $paramCount, null);
98 1543
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103 1513
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
104
    {
105 1513
        assert(is_int($column));
106
107 1513
        if (! isset(self::$_paramTypeMap[$type])) {
108
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
109
        }
110
111 1513
        $this->_bindedValues[$column] =& $variable;
112 1513
        $this->types[$column - 1]     = self::$_paramTypeMap[$type];
113
114 1513
        return true;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 1543
    public function bindValue($param, $value, $type = ParameterType::STRING)
121
    {
122 1543
        assert(is_int($param));
123
124 1543
        if (! isset(self::$_paramTypeMap[$type])) {
125
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
126
        }
127
128 1543
        $this->_values[$param]       = $value;
129 1543
        $this->_bindedValues[$param] =& $this->_values[$param];
130 1543
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
131
132 1543
        return true;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 1543
    public function execute($params = null)
139
    {
140 1543
        if ($this->_bindedValues !== null) {
141 1543
            if ($params !== null) {
142 1447
                if (! $this->bindUntypedValues($params)) {
143 1447
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
144
                }
145
            } else {
146 1543
                $this->bindTypedParameters();
147
            }
148
        }
149
150 1543 View Code Duplication
        if (! $this->_stmt->execute()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
151 1183
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
152
        }
153
154 1543
        if ($this->_columnNames === null) {
155 1543
            $meta = $this->_stmt->result_metadata();
156 1543
            if ($meta !== false) {
157 1537
                $fields = $meta->fetch_fields();
158 1537
                assert(is_array($fields));
159
160 1537
                $columnNames = [];
161 1537
                foreach ($fields as $col) {
162 1537
                    $columnNames[] = $col->name;
163
                }
164
165 1537
                $meta->free();
166
167 1537
                $this->_columnNames = $columnNames;
168
            } else {
169 1543
                $this->_columnNames = false;
170
            }
171
        }
172
173 1543
        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 1537
            $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 1537
            $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);
191
192 1537
            $refs = [];
193 1537
            foreach ($this->_rowBindedValues as $key => &$value) {
194 1537
                $refs[$key] =& $value;
195
            }
196
197 1537 View Code Duplication
            if (! $this->_stmt->bind_result(...$refs)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
199
            }
200
        }
201
202 1543
        $this->result = true;
203
204 1543
        return true;
205
    }
206
207
    /**
208
     * Binds parameters with known types previously bound to the statement
209
     */
210 1543
    private function bindTypedParameters() : void
211
    {
212 1543
        $streams = $values = [];
213 1543
        $types   = $this->types;
214
215 1543
        foreach ($this->_bindedValues as $parameter => $value) {
216 1543
            assert(is_int($parameter));
217
218 1543
            if (! isset($types[$parameter - 1])) {
219
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
220
            }
221
222 1543
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
223 1543
                if (is_resource($value)) {
224 1537
                    if (get_resource_type($value) !== 'stream') {
225
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
226
                    }
227
228 1537
                    $streams[$parameter] = $value;
229 1537
                    $values[$parameter]  = null;
230 1537
                    continue;
231
                }
232
233 1543
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
234
            }
235
236 1543
            $values[$parameter] = $value;
237
        }
238
239 1543 View Code Duplication
        if (! $this->_stmt->bind_param($types, ...$values)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
240
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
241
        }
242
243 1543
        $this->sendLongData($streams);
244 1543
    }
245
246
    /**
247
     * Handle $this->_longData after regular query parameters have been bound
248
     *
249
     * @param array<int, resource> $streams
250
     *
251
     * @throws MysqliException
252
     */
253 1543
    private function sendLongData(array $streams) : void
254
    {
255 1543
        foreach ($streams as $paramNr => $stream) {
256 1537
            while (! feof($stream)) {
257 1537
                $chunk = fread($stream, 8192);
258
259 1537
                if ($chunk === false) {
260
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
261
                }
262
263 1537 View Code Duplication
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
264
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
265
                }
266
            }
267
        }
268 1543
    }
269
270
    /**
271
     * Binds a array of values to bound parameters.
272
     *
273
     * @param mixed[] $values
274
     *
275
     * @return bool
276
     */
277 1447
    private function bindUntypedValues(array $values)
278
    {
279 1447
        $params = [];
280 1447
        $types  = str_repeat('s', count($values));
281
282 1447
        foreach ($values as &$v) {
283 1447
            $params[] =& $v;
284
        }
285
286 1447
        return $this->_stmt->bind_param($types, ...$params);
287
    }
288
289
    /**
290
     * @return mixed[]|false|null
291
     */
292 1537
    private function _fetch()
293
    {
294 1537
        $ret = $this->_stmt->fetch();
295
296 1537
        if ($ret === true) {
297 1537
            $values = [];
298 1537
            foreach ($this->_rowBindedValues as $v) {
299 1537
                $values[] = $v;
300
            }
301
302 1537
            return $values;
303
        }
304
305 1537
        return $ret;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311 1537
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
312
    {
313
        // do not try fetching from the statement if it's not expected to contain result
314
        // in order to prevent exceptional situation
315 1537
        if (! $this->result) {
316 299
            return false;
317
        }
318
319 1537
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
320
321 1537
        if ($fetchMode === FetchMode::COLUMN) {
322 233
            return $this->fetchColumn();
323
        }
324
325 1537
        $values = $this->_fetch();
326
327 1537
        if ($values === null) {
328 1537
            return false;
329
        }
330
331 1537 View Code Duplication
        if ($values === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
332
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
333
        }
334
335 1537
        if ($fetchMode === FetchMode::NUMERIC) {
336 1537
            return $values;
337
        }
338
339 1489
        assert(is_array($this->_columnNames));
340 1489
        $assoc = array_combine($this->_columnNames, $values);
341 1489
        assert(is_array($assoc));
342
343 1489
        switch ($fetchMode) {
344
            case FetchMode::ASSOCIATIVE:
345 1489
                return $assoc;
346
347
            case FetchMode::MIXED:
348 1471
                return $assoc + $values;
349
350
            case FetchMode::STANDARD_OBJECT:
351 1231
                return (object) $assoc;
352
353
            default:
354
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
355
        }
356
    }
357
358
    /**
359
     * {@inheritdoc}
360
     */
361 1537
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
362
    {
363 1537
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
364
365 1537
        $rows = [];
366
367 1537
        if ($fetchMode === FetchMode::COLUMN) {
368 1537
            while (($row = $this->fetchColumn()) !== false) {
369 1537
                $rows[] = $row;
370
            }
371
        } else {
372 1477
            while (($row = $this->fetch($fetchMode)) !== false) {
373 1477
                $rows[] = $row;
374
            }
375
        }
376
377 1537
        return $rows;
378
    }
379
380
    /**
381
     * {@inheritdoc}
382
     */
383 1537 View Code Duplication
    public function fetchColumn($columnIndex = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
384
    {
385 1537
        $row = $this->fetch(FetchMode::NUMERIC);
386
387 1537
        if ($row === false) {
388 1537
            return false;
389
        }
390
391 1537
        return $row[$columnIndex] ?? null;
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     */
397
    public function errorCode()
398
    {
399
        return $this->_stmt->errno;
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405
    public function errorInfo()
406
    {
407
        return $this->_stmt->error;
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 912
    public function closeCursor()
414
    {
415 912
        $this->_stmt->free_result();
416 912
        $this->result = false;
417
418 912
        return true;
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     */
424 1543
    public function rowCount()
425
    {
426 1543
        if ($this->_columnNames === false) {
427 1543
            return $this->_stmt->affected_rows;
428
        }
429
430
        return $this->_stmt->num_rows;
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436 912
    public function columnCount()
437
    {
438 912
        return $this->_stmt->field_count;
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444 1537
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
445
    {
446 1537
        $this->_defaultFetchMode = $fetchMode;
447
448 1537
        return true;
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454 1590
    public function getIterator()
455
    {
456 1590
        return new StatementIterator($this);
457
    }
458
}
459