Completed
Pull Request — master (#3610)
by Sergei
31:59 queued 28:53
created

MysqliStatement::execute()   C

Complexity

Conditions 11
Paths 21

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 11.0055

Importance

Changes 0
Metric Value
dl 0
loc 64
ccs 27
cts 28
cp 0.9643
rs 6.6387
c 0
b 0
f 0
cc 11
nc 21
nop 1
crap 11.0055

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
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\Mysqli;
6
7
use Doctrine\DBAL\Driver\DriverException;
8
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
9
use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
10
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
11
use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownFetchMode;
12
use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownType;
13
use Doctrine\DBAL\Driver\Statement;
14
use Doctrine\DBAL\Driver\StatementIterator;
15
use Doctrine\DBAL\Exception\InvalidArgumentException;
16
use Doctrine\DBAL\Exception\InvalidColumnIndex;
17
use Doctrine\DBAL\FetchMode;
18
use Doctrine\DBAL\ParameterType;
19
use IteratorAggregate;
20
use mysqli;
21
use mysqli_stmt;
22
use function array_combine;
23
use function array_fill;
24
use function array_key_exists;
25
use function assert;
26
use function count;
27
use function feof;
28
use function fread;
29
use function get_resource_type;
30
use function is_array;
31
use function is_int;
32
use function is_resource;
33
use function str_repeat;
34
35
class MysqliStatement implements IteratorAggregate, Statement
36
{
37
    /** @var string[] */
38
    protected static $_paramTypeMap = [
39
        ParameterType::STRING       => 's',
40
        ParameterType::BINARY       => 's',
41
        ParameterType::BOOLEAN      => 'i',
42
        ParameterType::NULL         => 's',
43
        ParameterType::INTEGER      => 'i',
44
        ParameterType::LARGE_OBJECT => 'b',
45
    ];
46
47
    /** @var mysqli */
48
    protected $_conn;
49
50
    /** @var mysqli_stmt */
51
    protected $_stmt;
52
53
    /** @var string[]|false|null */
54
    protected $_columnNames;
55
56
    /** @var mixed[] */
57
    protected $_rowBindedValues = [];
58
59
    /** @var mixed[] */
60
    protected $_bindedValues = [];
61
62
    /** @var string */
63
    protected $types;
64
65
    /**
66
     * Contains ref values for bindValue().
67
     *
68
     * @var mixed[]
69
     */
70
    protected $_values = [];
71
72
    /** @var int */
73
    protected $_defaultFetchMode = FetchMode::MIXED;
74
75
    /**
76
     * Indicates whether the statement is in the state when fetching results is possible
77
     *
78
     * @var bool
79
     */
80
    private $result = false;
81
82
    /**
83
     * @throws MysqliException
84
     */
85 2781
    public function __construct(mysqli $conn, string $sql)
86
    {
87 2781
        $this->_conn = $conn;
88
89 2781
        $stmt = $conn->prepare($sql);
90
91 2781
        if ($stmt === false) {
92 32
            throw ConnectionError::new($this->_conn);
93
        }
94
95 2749
        $this->_stmt = $stmt;
96
97 2749
        $paramCount = $this->_stmt->param_count;
98 2749
        if (0 >= $paramCount) {
99 2213
            return;
100
        }
101
102 1184
        $this->types         = str_repeat('s', $paramCount);
103 1184
        $this->_bindedValues = array_fill(1, $paramCount, null);
104 1184
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 192
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
110
    {
111 192
        assert(is_int($param));
112
113 192
        if (! isset(self::$_paramTypeMap[$type])) {
114
            throw UnknownType::new($type);
115
        }
116
117 192
        $this->_bindedValues[$param] =& $variable;
118 192
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
119 192
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 280
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
125
    {
126 280
        assert(is_int($param));
127
128 280
        if (! isset(self::$_paramTypeMap[$type])) {
129
            throw UnknownType::new($type);
130
        }
131
132 280
        $this->_values[$param]       = $value;
133 280
        $this->_bindedValues[$param] =& $this->_values[$param];
134 280
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
135 280
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140 2693
    public function execute(?array $params = null) : void
141
    {
142 2693
        if ($params !== null && count($params) > 0) {
143 768
            if (! $this->bindUntypedValues($params)) {
144 768
                throw StatementError::new($this->_stmt);
145
            }
146
        } else {
147 2469
            $this->bindTypedParameters();
148
        }
149
150 2685
        if (! $this->_stmt->execute()) {
151 48
            throw StatementError::new($this->_stmt);
152
        }
153
154 2677
        if ($this->_columnNames === null) {
155 2677
            $meta = $this->_stmt->result_metadata();
156 2677
            if ($meta !== false) {
157 2525
                $fields = $meta->fetch_fields();
158 2525
                assert(is_array($fields));
159
160 2525
                $columnNames = [];
161 2525
                foreach ($fields as $col) {
162 2525
                    $columnNames[] = $col->name;
163
                }
164
165 2525
                $meta->free();
166
167 2525
                $this->_columnNames = $columnNames;
168
            } else {
169 861
                $this->_columnNames = false;
170
            }
171
        }
172
173 2677
        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 2525
            $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 2525
            $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);
191
192 2525
            $refs = [];
193 2525
            foreach ($this->_rowBindedValues as $key => &$value) {
194 2525
                $refs[$key] =& $value;
195
            }
196
197 2525
            if (! $this->_stmt->bind_result(...$refs)) {
198
                throw StatementError::new($this->_stmt);
199
            }
200
        }
201
202 2677
        $this->result = true;
203 2677
    }
204
205
    /**
206
     * Binds parameters with known types previously bound to the statement
207
     *
208
     * @throws DriverException
209
     */
210 2469
    private function bindTypedParameters() : void
211
    {
212 2469
        $streams = $values = [];
213 2469
        $types   = $this->types;
214
215 2469
        foreach ($this->_bindedValues as $parameter => $value) {
216 472
            if (! isset($types[$parameter - 1])) {
217
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
218
            }
219
220 472
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
221 56
                if (is_resource($value)) {
222 24
                    if (get_resource_type($value) !== 'stream') {
223
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
224
                    }
225 24
                    $streams[$parameter] = $value;
226 24
                    $values[$parameter]  = null;
227 24
                    continue;
228
                }
229
230 40
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
231
            }
232
233 464
            $values[$parameter] = $value;
234
        }
235
236 2469
        if (count($values) > 0 && ! $this->_stmt->bind_param($types, ...$values)) {
237
            throw StatementError::new($this->_stmt);
238
        }
239
240 2469
        $this->sendLongData($streams);
241 2469
    }
242
243
    /**
244
     * Handle $this->_longData after regular query parameters have been bound
245
     *
246
     * @param resource[] $streams
247
     *
248
     * @throws MysqliException
249
     */
250 2469
    private function sendLongData(array $streams) : void
251
    {
252 2469
        foreach ($streams as $paramNr => $stream) {
253 24
            while (! feof($stream)) {
254 24
                $chunk = fread($stream, 8192);
255
256 24
                if ($chunk === false) {
257
                    throw FailedReadingStreamOffset::new($paramNr);
258
                }
259
260 24
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
261
                    throw StatementError::new($this->_stmt);
262
                }
263
            }
264
        }
265 2469
    }
266
267
    /**
268
     * Binds a array of values to bound parameters.
269
     *
270
     * @param mixed[] $values
271
     */
272 768
    private function bindUntypedValues(array $values) : bool
273
    {
274 768
        $params = [];
275 768
        $types  = str_repeat('s', count($values));
276
277 768
        foreach ($values as &$v) {
278 768
            $params[] =& $v;
279
        }
280
281 768
        return $this->_stmt->bind_param($types, ...$params);
282
    }
283
284
    /**
285
     * @return mixed[]|false|null
286
     */
287 2461
    private function _fetch()
288
    {
289 2461
        $ret = $this->_stmt->fetch();
290
291 2461
        if ($ret === true) {
292 2373
            $values = [];
293 2373
            foreach ($this->_rowBindedValues as $v) {
294 2373
                $values[] = $v;
295
            }
296
297 2373
            return $values;
298
        }
299
300 1157
        return $ret;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 2533
    public function fetch(?int $fetchMode = null, ...$args)
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 2533
        if (! $this->result) {
311 72
            return false;
312
        }
313
314 2461
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
315
316 2461
        if ($fetchMode === FetchMode::COLUMN) {
317 8
            return $this->fetchColumn();
318
        }
319
320 2461
        $values = $this->_fetch();
321
322 2461
        if ($values === null) {
323 1157
            return false;
324
        }
325
326 2373
        if ($values === false) {
327
            throw StatementError::new($this->_stmt);
328
        }
329
330 2373
        if ($fetchMode === FetchMode::NUMERIC) {
331 1669
            return $values;
332
        }
333
334 1325
        assert(is_array($this->_columnNames));
335 1325
        $assoc = array_combine($this->_columnNames, $values);
336 1325
        assert(is_array($assoc));
337
338 1325
        switch ($fetchMode) {
339
            case FetchMode::ASSOCIATIVE:
340 1301
                return $assoc;
341
342
            case FetchMode::MIXED:
343 16
                return $assoc + $values;
344
345
            case FetchMode::STANDARD_OBJECT:
346 8
                return (object) $assoc;
347
348
            default:
349
                throw UnknownFetchMode::new($fetchMode);
350
        }
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356 1045
    public function fetchAll(?int $fetchMode = null, ...$args) : array
357
    {
358 1045
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
359
360 1045
        $rows = [];
361
362 1045
        if ($fetchMode === FetchMode::COLUMN) {
363 80
            while (($row = $this->fetchColumn()) !== false) {
364 80
                $rows[] = $row;
365
            }
366
        } else {
367 965
            while (($row = $this->fetch($fetchMode)) !== false) {
368 893
                $rows[] = $row;
369
            }
370
        }
371
372 1045
        return $rows;
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 1661
    public function fetchColumn(int $columnIndex = 0)
379
    {
380 1661
        $row = $this->fetch(FetchMode::NUMERIC);
381
382 1661
        if ($row === false) {
383 128
            return false;
384
        }
385
386 1613
        if (! array_key_exists($columnIndex, $row)) {
387 16
            throw InvalidColumnIndex::new($columnIndex, count($row));
388
        }
389
390 1597
        return $row[$columnIndex];
391
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396 152
    public function closeCursor() : void
397
    {
398 152
        $this->_stmt->free_result();
399 152
        $this->result = false;
400 152
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405 824
    public function rowCount() : int
406
    {
407 824
        if ($this->_columnNames === false) {
408 824
            return $this->_stmt->affected_rows;
409
        }
410
411
        return $this->_stmt->num_rows;
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     */
417 32
    public function columnCount() : int
418
    {
419 32
        return $this->_stmt->field_count;
420
    }
421
422
    /**
423
     * {@inheritdoc}
424
     */
425 2621
    public function setFetchMode(int $fetchMode, ...$args) : void
426
    {
427 2621
        $this->_defaultFetchMode = $fetchMode;
428 2621
    }
429
430
    /**
431
     * {@inheritdoc}
432
     */
433 62
    public function getIterator()
434
    {
435 62
        return new StatementIterator($this);
436
    }
437
}
438