Failed Conditions
Push — 3.0.x ( 66f057...14f5f1 )
by Sergei
36s queued 17s
created

MysqliStatement::setFetchMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 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 IteratorAggregate;
11
use mysqli;
12
use mysqli_stmt;
13
use function array_combine;
14
use function array_fill;
15
use function assert;
16
use function count;
17
use function feof;
18
use function fread;
19
use function get_resource_type;
20
use function is_array;
21
use function is_int;
22
use function is_resource;
23
use function sprintf;
24
use function str_repeat;
25
26
class MysqliStatement implements IteratorAggregate, Statement
27
{
28
    /** @var string[] */
29
    protected static $_paramTypeMap = [
30
        ParameterType::STRING       => 's',
31
        ParameterType::BINARY       => 's',
32
        ParameterType::BOOLEAN      => 'i',
33
        ParameterType::NULL         => 's',
34
        ParameterType::INTEGER      => 'i',
35
        ParameterType::LARGE_OBJECT => 'b',
36
    ];
37
38
    /** @var mysqli */
39
    protected $_conn;
40
41
    /** @var mysqli_stmt */
42
    protected $_stmt;
43
44
    /** @var string[]|false|null */
45
    protected $_columnNames;
46
47
    /** @var mixed[] */
48
    protected $_rowBindedValues = [];
49
50
    /** @var mixed[] */
51
    protected $_bindedValues;
52
53
    /** @var string */
54
    protected $types;
55
56
    /**
57
     * Contains ref values for bindValue().
58
     *
59
     * @var mixed[]
60
     */
61
    protected $_values = [];
62
63
    /** @var int */
64
    protected $_defaultFetchMode = FetchMode::MIXED;
65
66
    /**
67
     * Indicates whether the statement is in the state when fetching results is possible
68
     *
69
     * @var bool
70
     */
71
    private $result = false;
72
73
    /**
74
     * @param string $prepareString
75
     *
76
     * @throws MysqliException
77
     */
78
    public function __construct(mysqli $conn, $prepareString)
79
    {
80
        $this->_conn = $conn;
81
82
        $stmt = $conn->prepare($prepareString);
83
84
        if ($stmt === false) {
85
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
86
        }
87
88
        $this->_stmt = $stmt;
89
90
        $paramCount = $this->_stmt->param_count;
91
        if (0 >= $paramCount) {
92
            return;
93
        }
94
95
        $this->types         = str_repeat('s', $paramCount);
96
        $this->_bindedValues = array_fill(1, $paramCount, null);
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
103
    {
104
        assert(is_int($column));
105
106
        if (! isset(self::$_paramTypeMap[$type])) {
107
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
108
        }
109
110
        $this->_bindedValues[$column] =& $variable;
111
        $this->types[$column - 1]     = self::$_paramTypeMap[$type];
112
113
        return true;
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function bindValue($param, $value, $type = ParameterType::STRING)
120
    {
121
        assert(is_int($param));
122
123
        if (! isset(self::$_paramTypeMap[$type])) {
124
            throw new MysqliException(sprintf("Unknown type: '%s'", $type));
125
        }
126
127
        $this->_values[$param]       = $value;
128
        $this->_bindedValues[$param] =& $this->_values[$param];
129
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
130
131
        return true;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function execute($params = null)
138
    {
139
        if ($this->_bindedValues !== null) {
140
            if ($params !== null) {
141
                if (! $this->bindUntypedValues($params)) {
142
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
143
                }
144
            } else {
145
                $this->bindTypedParameters();
146
            }
147
        }
148
149
        if (! $this->_stmt->execute()) {
150
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
151
        }
152
153
        if ($this->_columnNames === null) {
154
            $meta = $this->_stmt->result_metadata();
155
            if ($meta !== false) {
156
                $fields = $meta->fetch_fields();
157
                assert(is_array($fields));
158
159
                $columnNames = [];
160
                foreach ($fields as $col) {
161
                    $columnNames[] = $col->name;
162
                }
163
164
                $meta->free();
165
166
                $this->_columnNames = $columnNames;
167
            } else {
168
                $this->_columnNames = false;
169
            }
170
        }
171
172
        if ($this->_columnNames !== false) {
173
            // Store result of every execution which has it. Otherwise it will be impossible
174
            // to execute a new statement in case if the previous one has non-fetched rows
175
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
176
            $this->_stmt->store_result();
177
178
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
179
            // it will have to allocate as much memory as it may be needed for the given column type
180
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
181
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
182
            //
183
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
184
            // previously called on the statement, the values are unbound making the statement unusable.
185
            //
186
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
187
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
188
            // to the length of the ones fetched during the previous execution.
189
            $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

189
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
190
191
            $refs = [];
192
            foreach ($this->_rowBindedValues as $key => &$value) {
193
                $refs[$key] =& $value;
194
            }
195
196
            if (! $this->_stmt->bind_result(...$refs)) {
197
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
198
            }
199
        }
200
201
        $this->result = true;
202
203
        return true;
204
    }
205
206
    /**
207
     * Binds parameters with known types previously bound to the statement
208
     */
209
    private function bindTypedParameters() : void
210
    {
211
        $streams = $values = [];
212
        $types   = $this->types;
213
214
        foreach ($this->_bindedValues as $parameter => $value) {
215
            assert(is_int($parameter));
216
217
            if (! isset($types[$parameter - 1])) {
218
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
219
            }
220
221
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
222
                if (is_resource($value)) {
223
                    if (get_resource_type($value) !== 'stream') {
224
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
225
                    }
226
                    $streams[$parameter] = $value;
227
                    $values[$parameter]  = null;
228
                    continue;
229
                }
230
231
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
232
            }
233
234
            $values[$parameter] = $value;
235
        }
236
237
        if (! $this->_stmt->bind_param($types, ...$values)) {
238
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
239
        }
240
241
        $this->sendLongData($streams);
242
    }
243
244
    /**
245
     * Handle $this->_longData after regular query parameters have been bound
246
     *
247
     * @param array<int, resource> $streams
248
     *
249
     * @throws MysqliException
250
     */
251
    private function sendLongData(array $streams) : void
252
    {
253
        foreach ($streams as $paramNr => $stream) {
254
            while (! feof($stream)) {
255
                $chunk = fread($stream, 8192);
256
257
                if ($chunk === false) {
258
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
259
                }
260
261
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
262
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
263
                }
264
            }
265
        }
266
    }
267
268
    /**
269
     * Binds a array of values to bound parameters.
270
     *
271
     * @param mixed[] $values
272
     *
273
     * @return bool
274
     */
275
    private function bindUntypedValues(array $values)
276
    {
277
        $params = [];
278
        $types  = str_repeat('s', count($values));
279
280
        foreach ($values as &$v) {
281
            $params[] =& $v;
282
        }
283
284
        return $this->_stmt->bind_param($types, ...$params);
285
    }
286
287
    /**
288
     * @return mixed[]|false|null
289
     */
290
    private function _fetch()
291
    {
292
        $ret = $this->_stmt->fetch();
293
294
        if ($ret === true) {
295
            $values = [];
296
            foreach ($this->_rowBindedValues as $v) {
297
                $values[] = $v;
298
            }
299
300
            return $values;
301
        }
302
303
        return $ret;
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function fetch($fetchMode = null, ...$args)
310
    {
311
        // do not try fetching from the statement if it's not expected to contain result
312
        // in order to prevent exceptional situation
313
        if (! $this->result) {
314
            return false;
315
        }
316
317
        $fetchMode = $fetchMode ?? $this->_defaultFetchMode;
318
319
        if ($fetchMode === FetchMode::COLUMN) {
320
            return $this->fetchColumn();
321
        }
322
323
        $values = $this->_fetch();
324
325
        if ($values === null) {
326
            return false;
327
        }
328
329
        if ($values === false) {
330
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
331
        }
332
333
        if ($fetchMode === FetchMode::NUMERIC) {
334
            return $values;
335
        }
336
337
        assert(is_array($this->_columnNames));
338
        $assoc = array_combine($this->_columnNames, $values);
339
        assert(is_array($assoc));
340
341
        switch ($fetchMode) {
342
            case FetchMode::ASSOCIATIVE:
343
                return $assoc;
344
345
            case FetchMode::MIXED:
346
                return $assoc + $values;
347
348
            case FetchMode::STANDARD_OBJECT:
349
                return (object) $assoc;
350
351
            default:
352
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
353
        }
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359
    public function fetchAll($fetchMode = null, ...$args)
360
    {
361
        $fetchMode = $fetchMode ?? $this->_defaultFetchMode;
362
363
        $rows = [];
364
365
        if ($fetchMode === FetchMode::COLUMN) {
366
            while (($row = $this->fetchColumn()) !== false) {
367
                $rows[] = $row;
368
            }
369
        } else {
370
            while (($row = $this->fetch($fetchMode)) !== false) {
371
                $rows[] = $row;
372
            }
373
        }
374
375
        return $rows;
376
    }
377
378
    /**
379
     * {@inheritdoc}
380
     */
381
    public function fetchColumn($columnIndex = 0)
382
    {
383
        $row = $this->fetch(FetchMode::NUMERIC);
384
385
        if ($row === false) {
386
            return false;
387
        }
388
389
        return $row[$columnIndex] ?? null;
390
    }
391
392
    /**
393
     * {@inheritdoc}
394
     */
395
    public function errorCode()
396
    {
397
        return $this->_stmt->errno;
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403
    public function errorInfo()
404
    {
405
        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...
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411
    public function closeCursor()
412
    {
413
        $this->_stmt->free_result();
414
        $this->result = false;
415
416
        return true;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function rowCount() : int
423
    {
424
        if ($this->_columnNames === false) {
425
            return $this->_stmt->affected_rows;
426
        }
427
428
        return $this->_stmt->num_rows;
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     */
434
    public function columnCount()
435
    {
436
        return $this->_stmt->field_count;
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     */
442
    public function setFetchMode($fetchMode, ...$args)
443
    {
444
        $this->_defaultFetchMode = $fetchMode;
445
446
        return true;
447
    }
448
449
    /**
450
     * {@inheritdoc}
451
     */
452
    public function getIterator()
453
    {
454
        return new StatementIterator($this);
455
    }
456
}
457