Completed
Push — develop ( 40c4f3...ed7ad1 )
by Sergei
62:39
created

MysqliStatement::setFetchMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 2
crap 2
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 function array_combine;
11
use function array_fill;
12
use function count;
13
use function feof;
14
use function fread;
15
use function get_resource_type;
16
use function is_resource;
17
use function str_repeat;
18
19
/**
20
 * @author Kim Hemsø Rasmussen <[email protected]>
21
 */
22
class MysqliStatement implements \IteratorAggregate, Statement
23
{
24
    /**
25
     * @var array
26
     */
27
    protected static $_paramTypeMap = [
28
        ParameterType::STRING       => 's',
29
        ParameterType::BINARY       => 's',
30
        ParameterType::BOOLEAN      => 'i',
31
        ParameterType::NULL         => 's',
32
        ParameterType::INTEGER      => 'i',
33
        ParameterType::LARGE_OBJECT => 'b',
34
    ];
35
36
    /**
37
     * @var \mysqli
38
     */
39
    protected $_conn;
40
41
    /**
42
     * @var \mysqli_stmt
43
     */
44
    protected $_stmt;
45
46
    /**
47
     * @var null|boolean|array
48
     */
49
    protected $_columnNames;
50
51
    /**
52
     * @var null|array
53
     */
54
    protected $_rowBindedValues;
55
56
    /**
57
     * @var array
58
     */
59
    protected $_bindedValues;
60
61
    /**
62
     * @var string
63
     */
64
    protected $types;
65
66
    /**
67
     * Contains ref values for bindValue().
68
     *
69
     * @var array
70
     */
71
    protected $_values = [];
72
73
    /**
74
     * @var int
75
     */
76
    protected $_defaultFetchMode = FetchMode::MIXED;
77
78
    /**
79
     * Indicates whether the statement is in the state when fetching results is possible
80
     *
81
     * @var bool
82
     */
83
    private $result = false;
84
85
    /**
86
     * @param \mysqli $conn
87
     * @param string  $prepareString
88
     *
89 759
     * @throws \Doctrine\DBAL\Driver\Mysqli\MysqliException
90
     */
91 759
    public function __construct(\mysqli $conn, $prepareString)
92 759
    {
93 759
        $this->_conn = $conn;
94 12
        $this->_stmt = $conn->prepare($prepareString);
95
        if (false === $this->_stmt) {
0 ignored issues
show
introduced by
The condition $this->_stmt is always false. If $this->_stmt can have other possible types, add them to lib/Doctrine/DBAL/Driver...qli/MysqliStatement.php:42
Loading history...
96
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
97 747
        }
98 747
99 366
        $paramCount = $this->_stmt->param_count;
100 366
        if (0 < $paramCount) {
101
            $this->types = str_repeat('s', $paramCount);
102 747
            $this->_bindedValues = array_fill(1, $paramCount, null);
103
        }
104
    }
105
106
    /**
107 21
     * {@inheritdoc}
108
     */
109 21
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
110
    {
111
        if (null === $type) {
112 21
            $type = 's';
113 21
        } else {
114
            if (isset(self::$_paramTypeMap[$type])) {
115
                $type = self::$_paramTypeMap[$type];
116
            } else {
117
                throw new MysqliException("Unknown type: '{$type}'");
118
            }
119 21
        }
120 21
121
        $this->_bindedValues[$column] =& $variable;
122 21
        $this->types[$column - 1] = $type;
123
124
        return true;
125
    }
126
127
    /**
128 102
     * {@inheritdoc}
129
     */
130 102
    public function bindValue($param, $value, $type = ParameterType::STRING)
131
    {
132
        if (null === $type) {
133 102
            $type = 's';
134 102
        } else {
135
            if (isset(self::$_paramTypeMap[$type])) {
136
                $type = self::$_paramTypeMap[$type];
137
            } else {
138
                throw new MysqliException("Unknown type: '{$type}'");
139
            }
140 102
        }
141 102
142 102
        $this->_values[$param] = $value;
143
        $this->_bindedValues[$param] =& $this->_values[$param];
144 102
        $this->types[$param - 1] = $type;
145
146
        return true;
147
    }
148
149
    /**
150 723
     * {@inheritdoc}
151
     */
152 723
    public function execute($params = null)
153 366
    {
154 264
        if (null !== $this->_bindedValues) {
155 264
            if (null !== $params) {
156
                if ( ! $this->_bindValues($params)) {
157
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
158 123
                }
159
            } else {
160
                list($types, $values, $streams) = $this->separateBoundValues();
161
                if (! $this->_stmt->bind_param($types, ...$values)) {
0 ignored issues
show
Bug introduced by
It seems like $types can also be of type array<integer|string,mixed>; however, parameter $types of mysqli_stmt::bind_param() does only seem to accept string, 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

161
                if (! $this->_stmt->bind_param(/** @scrutinizer ignore-type */ $types, ...$values)) {
Loading history...
162
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
163
                }
164 723
                $this->sendLongData($streams);
165 18
            }
166
        }
167
168 720
        if ( ! $this->_stmt->execute()) {
169 720
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
170 720
        }
171 663
172 663
        if (null === $this->_columnNames) {
173 663
            $meta = $this->_stmt->result_metadata();
174
            if (false !== $meta) {
175 663
                $columnNames = [];
176
                foreach ($meta->fetch_fields() as $col) {
177 663
                    $columnNames[] = $col->name;
178
                }
179 286
                $meta->free();
180
181
                $this->_columnNames = $columnNames;
182
            } else {
183 720
                $this->_columnNames = false;
184
            }
185
        }
186
187 663
        if (false !== $this->_columnNames) {
188
            // Store result of every execution which has it. Otherwise it will be impossible
189
            // to execute a new statement in case if the previous one has non-fetched rows
190
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
191
            $this->_stmt->store_result();
192
193
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
194
            // it will have to allocate as much memory as it may be needed for the given column type
195
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
196
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
197
            //
198
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
199
            // previously called on the statement, the values are unbound making the statement unusable.
200 663
            //
201
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
202 663
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
203 663
            // to the length of the ones fetched during the previous execution.
204 663
            $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

204
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
205
206
            $refs = [];
207 663
            foreach ($this->_rowBindedValues as $key => &$value) {
208
                $refs[$key] =& $value;
209
            }
210
211
            if (! $this->_stmt->bind_result(...$refs)) {
212 720
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
213
            }
214 720
        }
215
216
        $this->result = true;
217
218
        return true;
219
    }
220
221
    /**
222
     * Split $this->_bindedValues into those values that need to be sent using mysqli::send_long_data()
223
     * and those that can be bound the usual way.
224 264
     *
225
     * @return array<int, array<int|string, mixed>|string>
226 264
     */
227 264
    private function separateBoundValues()
228 264
    {
229
        $streams = $values = [];
230 264
        $types   = $this->types;
231 264
232
        foreach ($this->_bindedValues as $parameter => $value) {
233
            if (! isset($types[$parameter - 1])) {
234 264
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
235
            }
236
237
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
238
                if (is_resource($value)) {
239
                    if (get_resource_type($value) !== 'stream') {
240 639
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
241
                    }
242 639
                    $streams[$parameter] = $value;
243
                    $values[$parameter]  = null;
244 639
                    continue;
245 606
                } else {
246 606
                    $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
247 606
                }
248
            }
249
250 606
            $values[$parameter] = $value;
251
        }
252
253 372
        return [$types, $values, $streams];
254
    }
255
256
    /**
257
     * Handle $this->_longData after regular query parameters have been bound
258
     *
259 666
     * @throws MysqliException
260
     */
261
    private function sendLongData($streams)
262
    {
263 666
        foreach ($streams as $paramNr => $stream) {
264 27
            while (! feof($stream)) {
265
                $chunk = fread($stream, 8192);
266
267 639
                if ($chunk === false) {
268
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
269 639
                }
270 3
271
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
272
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
273 639
                }
274 639
            }
275 372
        }
276
    }
277
278 606
    /**
279
     * Binds a array of values to bound parameters.
280
     *
281
     * @param array $values
282 606
     *
283
     * @return bool
284 156
     */
285
    private function _bindValues($values)
286
    {
287 447
        $params = [];
288
        $types = str_repeat('s', count($values));
289
290 6
        foreach ($values as &$v) {
291 6
            $params[] =& $v;
292
        }
293 6
294
        return $this->_stmt->bind_param($types, ...$params);
295
    }
296 3
297 3
    /**
298
     * @return mixed[]|false
299 3
     */
300 3
    private function _fetch()
301
    {
302
        $ret = $this->_stmt->fetch();
303 3
304
        if (true === $ret) {
305
            $values = [];
306
            foreach ($this->_rowBindedValues as $v) {
307
                $values[] = $v;
308
            }
309
310
            return $values;
311
        }
312
313 327
        return $ret;
314
    }
315 327
316
    /**
317 327
     * {@inheritdoc}
318
     */
319 327
    public function fetch($fetchMode = null, ...$args)
320 12
    {
321 12
        // do not try fetching from the statement if it's not expected to contain result
322
        // in order to prevent exceptional situation
323
        if (!$this->result) {
324 315
            return false;
325 294
        }
326
327
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
328
329 327
        if ($fetchMode === FetchMode::COLUMN) {
330
            return $this->fetchColumn();
331
        }
332
333
        $values = $this->_fetch();
334
        if (null === $values) {
0 ignored issues
show
introduced by
The condition null === $values is always false.
Loading history...
335 156
            return false;
336
        }
337 156
338
        if (false === $values) {
339 156
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
340 30
        }
341
342
        switch ($fetchMode) {
343 138
            case FetchMode::NUMERIC:
344
                return $values;
345
346
            case FetchMode::ASSOCIATIVE:
347
                return array_combine($this->_columnNames, $values);
0 ignored issues
show
Bug introduced by
It seems like $this->_columnNames can also be of type null and boolean; however, parameter $keys of array_combine() does only seem to accept 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

347
                return array_combine(/** @scrutinizer ignore-type */ $this->_columnNames, $values);
Loading history...
348
349
            case FetchMode::MIXED:
350
                $ret = array_combine($this->_columnNames, $values);
351
                $ret += $values;
352
353
                return $ret;
354
355
            case FetchMode::STANDARD_OBJECT:
356
                $assoc = array_combine($this->_columnNames, $values);
357
                $ret = new \stdClass();
358
359
                foreach ($assoc as $column => $value) {
360
                    $ret->$column = $value;
361
                }
362
363
                return $ret;
364
365 57
            default:
366
                throw new MysqliException("Unknown fetch type '{$fetchMode}'");
367 57
        }
368 57
    }
369
370 57
    /**
371
     * {@inheritdoc}
372
     */
373
    public function fetchAll($fetchMode = null, ...$args)
374
    {
375
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
376 279
377
        $rows = [];
378 279
379 279
        if ($fetchMode === FetchMode::COLUMN) {
380
            while (($row = $this->fetchColumn()) !== false) {
381
                $rows[] = $row;
382
            }
383
        } else {
384
            while (($row = $this->fetch($fetchMode)) !== false) {
385
                $rows[] = $row;
386
            }
387
        }
388 12
389
        return $rows;
390 12
    }
391
392
    /**
393
     * {@inheritdoc}
394
     */
395
    public function fetchColumn($columnIndex = 0)
396 699
    {
397
        $row = $this->fetch(FetchMode::NUMERIC);
398 699
399
        if (false === $row) {
400 699
            return false;
401
        }
402
403
        return $row[$columnIndex] ?? null;
404
    }
405
406 35
    /**
407
     * {@inheritdoc}
408 35
     */
409
    public function errorCode()
410
    {
411
        return $this->_stmt->errno;
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     */
417
    public function errorInfo()
418
    {
419
        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.

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...
420
    }
421
422
    /**
423
     * {@inheritdoc}
424
     */
425
    public function closeCursor()
426
    {
427
        $this->_stmt->free_result();
428
        $this->result = false;
429
430
        return true;
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436
    public function rowCount() : int
437
    {
438
        if (false === $this->_columnNames) {
439
            return $this->_stmt->affected_rows;
440
        }
441
442
        return $this->_stmt->num_rows;
443
    }
444
445
    /**
446
     * {@inheritdoc}
447
     */
448
    public function columnCount()
449
    {
450
        return $this->_stmt->field_count;
451
    }
452
453
    /**
454
     * {@inheritdoc}
455
     */
456
    public function setFetchMode($fetchMode, ...$args)
457
    {
458
        $this->_defaultFetchMode = $fetchMode;
459
460
        return true;
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466
    public function getIterator()
467
    {
468
        return new StatementIterator($this);
469
    }
470
}
471