Completed
Pull Request — master (#3610)
by Sergei
03:27 queued 10s
created

MysqliStatement::execute()   C

Complexity

Conditions 11
Paths 21

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11.0061

Importance

Changes 0
Metric Value
dl 0
loc 64
ccs 26
cts 27
cp 0.963
rs 6.6387
c 0
b 0
f 0
cc 11
nc 21
nop 1
crap 11.0061

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 1816
     */
80
    private $result = false;
81 1816
82
    /**
83 1816
     * @throws MysqliException
84
     */
85 1816
    public function __construct(mysqli $conn, string $sql)
86 1389
    {
87
        $this->_conn = $conn;
88
89 1816
        $stmt = $conn->prepare($sql);
90
91 1816
        if ($stmt === false) {
92 1816
            throw ConnectionError::new($this->_conn);
93 1809
        }
94
95
        $this->_stmt = $stmt;
96 1816
97 1816
        $paramCount = $this->_stmt->param_count;
98 1816
        if (0 >= $paramCount) {
99
            return;
100
        }
101
102
        $this->types         = str_repeat('s', $paramCount);
103 1781
        $this->_bindedValues = array_fill(1, $paramCount, null);
104
    }
105 1781
106
    /**
107 1781
     * {@inheritdoc}
108
     */
109
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
110
    {
111 1781
        assert(is_int($param));
112 1781
113
        if (! isset(self::$_paramTypeMap[$type])) {
114 1781
            throw UnknownType::new($type);
115
        }
116
117
        $this->_bindedValues[$param] =& $variable;
118
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
119
    }
120 1816
121
    /**
122 1816
     * {@inheritdoc}
123
     */
124 1816
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
125
    {
126
        assert(is_int($param));
127
128 1816
        if (! isset(self::$_paramTypeMap[$type])) {
129 1816
            throw UnknownType::new($type);
130 1816
        }
131
132 1816
        $this->_values[$param]       = $value;
133
        $this->_bindedValues[$param] =& $this->_values[$param];
134
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
135
    }
136
137
    /**
138 1816
     * {@inheritdoc}
139
     */
140 1816
    public function execute(?array $params = null) : void
141 1816
    {
142 1753
        if ($params !== null && count($params) > 0) {
143 1753
            if (! $this->bindUntypedValues($params)) {
144
                throw StatementError::new($this->_stmt);
145
            }
146 1816
        } else {
147
            $this->bindTypedParameters();
148
        }
149
150 1816
        if (! $this->_stmt->execute()) {
151 1396
            throw StatementError::new($this->_stmt);
152
        }
153
154 1816
        if ($this->_columnNames === null) {
155 1816
            $meta = $this->_stmt->result_metadata();
156 1816
            if ($meta !== false) {
157 1809
                $fields = $meta->fetch_fields();
158 1809
                assert(is_array($fields));
159
160 1809
                $columnNames = [];
161 1809
                foreach ($fields as $col) {
162 1809
                    $columnNames[] = $col->name;
163
                }
164
165 1809
                $meta->free();
166
167 1809
                $this->_columnNames = $columnNames;
168
            } else {
169 1816
                $this->_columnNames = false;
170
            }
171
        }
172
173 1816
        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 1809
            $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 1809
            $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);
191
192 1809
            $refs = [];
193 1809
            foreach ($this->_rowBindedValues as $key => &$value) {
194 1809
                $refs[$key] =& $value;
195
            }
196
197 1809
            if (! $this->_stmt->bind_result(...$refs)) {
198
                throw StatementError::new($this->_stmt);
199
            }
200
        }
201
202 1816
        $this->result = true;
203
    }
204 1816
205
    /**
206
     * Binds parameters with known types previously bound to the statement
207
     *
208
     * @throws DriverException
209
     */
210 1816
    private function bindTypedParameters() : void
211
    {
212 1816
        $streams = $values = [];
213 1816
        $types   = $this->types;
214
215 1816
        foreach ($this->_bindedValues as $parameter => $value) {
216 1816
            if (! isset($types[$parameter - 1])) {
217
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
218
            }
219
220 1816
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
221 1816
                if (is_resource($value)) {
222 1809
                    if (get_resource_type($value) !== 'stream') {
223
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
224
                    }
225 1809
                    $streams[$parameter] = $value;
226 1809
                    $values[$parameter]  = null;
227 1809
                    continue;
228
                }
229
230 1816
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
231
            }
232
233 1816
            $values[$parameter] = $value;
234
        }
235
236 1816
        if (count($values) > 0 && ! $this->_stmt->bind_param($types, ...$values)) {
237
            throw StatementError::new($this->_stmt);
238
        }
239
240 1816
        $this->sendLongData($streams);
241 1816
    }
242
243
    /**
244
     * Handle $this->_longData after regular query parameters have been bound
245
     *
246
     * @param resource[] $streams
247
     *
248 1816
     * @throws MysqliException
249
     */
250 1816
    private function sendLongData(array $streams) : void
251 1809
    {
252 1809
        foreach ($streams as $paramNr => $stream) {
253
            while (! feof($stream)) {
254 1809
                $chunk = fread($stream, 8192);
255
256
                if ($chunk === false) {
257
                    throw FailedReadingStreamOffset::new($paramNr);
258 1809
                }
259
260
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
261
                    throw StatementError::new($this->_stmt);
262
                }
263 1816
            }
264
        }
265
    }
266
267
    /**
268
     * Binds a array of values to bound parameters.
269
     *
270
     * @param mixed[] $values
271
     */
272 1753
    private function bindUntypedValues(array $values) : bool
273
    {
274 1753
        $params = [];
275 1753
        $types  = str_repeat('s', count($values));
276
277 1753
        foreach ($values as &$v) {
278 1753
            $params[] =& $v;
279
        }
280
281 1753
        return $this->_stmt->bind_param($types, ...$params);
282
    }
283
284
    /**
285
     * @return mixed[]|false|null
286
     */
287 1809
    private function _fetch()
288
    {
289 1809
        $ret = $this->_stmt->fetch();
290
291 1809
        if ($ret === true) {
292 1809
            $values = [];
293 1809
            foreach ($this->_rowBindedValues as $v) {
294 1809
                $values[] = $v;
295
            }
296
297 1809
            return $values;
298
        }
299
300 1809
        return $ret;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 1809
    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 1809
        if (! $this->result) {
311 346
            return false;
312
        }
313
314 1809
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
315
316 1809
        if ($fetchMode === FetchMode::COLUMN) {
317 269
            return $this->fetchColumn();
318
        }
319
320 1809
        $values = $this->_fetch();
321
322 1809
        if ($values === null) {
323 1809
            return false;
324
        }
325
326 1809
        if ($values === false) {
327
            throw StatementError::new($this->_stmt);
328
        }
329
330 1809
        if ($fetchMode === FetchMode::NUMERIC) {
331 1809
            return $values;
332
        }
333
334 1753
        assert(is_array($this->_columnNames));
335 1753
        $assoc = array_combine($this->_columnNames, $values);
336 1753
        assert(is_array($assoc));
337
338 1753
        switch ($fetchMode) {
339
            case FetchMode::ASSOCIATIVE:
340 1753
                return $assoc;
341
342
            case FetchMode::MIXED:
343 1732
                return $assoc + $values;
344
345
            case FetchMode::STANDARD_OBJECT:
346 1452
                return (object) $assoc;
347
348
            default:
349
                throw UnknownFetchMode::new($fetchMode);
350
        }
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356 1809
    public function fetchAll(?int $fetchMode = null, ...$args) : array
357
    {
358 1809
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
359
360 1809
        $rows = [];
361
362 1809
        if ($fetchMode === FetchMode::COLUMN) {
363 1809
            while (($row = $this->fetchColumn()) !== false) {
364 1809
                $rows[] = $row;
365
            }
366
        } else {
367 1739
            while (($row = $this->fetch($fetchMode)) !== false) {
368 1739
                $rows[] = $row;
369
            }
370
        }
371
372 1809
        return $rows;
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 1809
    public function fetchColumn(int $columnIndex = 0)
379
    {
380 1809
        $row = $this->fetch(FetchMode::NUMERIC);
381
382 1809
        if ($row === false) {
383 1809
            return false;
384
        }
385
386 1809
        if (! array_key_exists($columnIndex, $row)) {
387
            throw InvalidColumnIndex::new($columnIndex, count($row));
388
        }
389
390
        return $row[$columnIndex];
391
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396
    public function closeCursor() : void
397
    {
398
        $this->_stmt->free_result();
399
        $this->result = false;
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405
    public function rowCount() : int
406
    {
407
        if ($this->_columnNames === false) {
408 1066
            return $this->_stmt->affected_rows;
409
        }
410 1066
411 1066
        return $this->_stmt->num_rows;
412
    }
413 1066
414
    /**
415
     * {@inheritdoc}
416
     */
417
    public function columnCount() : int
418
    {
419 1816
        return $this->_stmt->field_count;
420
    }
421 1816
422 1816
    /**
423
     * {@inheritdoc}
424
     */
425
    public function setFetchMode(int $fetchMode, ...$args) : void
426
    {
427
        $this->_defaultFetchMode = $fetchMode;
428
    }
429
430
    /**
431 1066
     * {@inheritdoc}
432
     */
433 1066
    public function getIterator()
434
    {
435
        return new StatementIterator($this);
436
    }
437
}
438