Failed Conditions
Push — master ( ac0e13...24dbc4 )
by Sergei
22s queued 15s
created

MysqliStatement::bindValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 3
dl 0
loc 11
rs 10
c 0
b 0
f 0
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 stdClass;
23
use function array_combine;
24
use function array_fill;
25
use function array_key_exists;
26
use function array_map;
27
use function assert;
28
use function count;
29
use function feof;
30
use function fread;
31
use function get_resource_type;
32
use function is_array;
33
use function is_int;
34
use function is_resource;
35
use function str_repeat;
36
37
final class MysqliStatement implements IteratorAggregate, Statement
38
{
39
    /** @var string[] */
40
    private static $paramTypeMap = [
41
        ParameterType::STRING       => 's',
42
        ParameterType::BINARY       => 's',
43
        ParameterType::BOOLEAN      => 'i',
44
        ParameterType::NULL         => 's',
45
        ParameterType::INTEGER      => 'i',
46
        ParameterType::LARGE_OBJECT => 'b',
47
    ];
48
49
    /** @var mysqli */
50
    private $conn;
51
52
    /** @var mysqli_stmt */
53
    private $stmt;
54
55
    /**
56
     * Whether the statement result metadata has been fetched.
57
     *
58
     * @var bool
59
     */
60
    private $metadataFetched = false;
61
62
    /**
63
     * Whether the statement result has columns. The property should be used only after the result metadata
64
     * has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined.
65
     *
66
     * @var bool
67
     */
68
    private $hasColumns = false;
69
70
    /**
71
     * Mapping of statement result column indexes to their names. The property should be used only
72
     * if the statement result has columns ({@see $hasColumns}). Otherwise, the property value is undetermined.
73
     *
74
     * @var array<int,string>
75
     */
76
    private $columnNames = [];
77
78
    /** @var mixed[] */
79
    private $rowBoundValues = [];
80
81
    /** @var mixed[] */
82
    private $boundValues = [];
83
84
    /** @var string */
85
    private $types;
86
87
    /**
88
     * Contains ref values for bindValue().
89
     *
90
     * @var mixed[]
91
     */
92
    private $values = [];
93
94
    /** @var int */
95
    private $defaultFetchMode = FetchMode::MIXED;
96
97
    /**
98
     * Indicates whether the statement is in the state when fetching results is possible
99
     *
100
     * @var bool
101
     */
102
    private $result = false;
103
104
    /**
105
     * @throws MysqliException
106
     */
107
    public function __construct(mysqli $conn, string $sql)
108
    {
109
        $this->conn = $conn;
110
111
        $stmt = $conn->prepare($sql);
112
113
        if ($stmt === false) {
114
            throw ConnectionError::new($this->conn);
115
        }
116
117
        $this->stmt = $stmt;
118
119
        $paramCount = $this->stmt->param_count;
120
        if (0 >= $paramCount) {
121
            return;
122
        }
123
124
        $this->types       = str_repeat('s', $paramCount);
125
        $this->boundValues = array_fill(1, $paramCount, null);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
132
    {
133
        assert(is_int($param));
134
135
        if (! isset(self::$paramTypeMap[$type])) {
136
            throw UnknownType::new($type);
137
        }
138
139
        $this->boundValues[$param] =& $variable;
140
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
147
    {
148
        assert(is_int($param));
149
150
        if (! isset(self::$paramTypeMap[$type])) {
151
            throw UnknownType::new($type);
152
        }
153
154
        $this->values[$param]      = $value;
155
        $this->boundValues[$param] =& $this->values[$param];
156
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function execute(?array $params = null) : void
163
    {
164
        if ($params !== null && count($params) > 0) {
165
            if (! $this->bindUntypedValues($params)) {
166
                throw StatementError::new($this->stmt);
167
            }
168
        } else {
169
            $this->bindTypedParameters();
170
        }
171
172
        if (! $this->stmt->execute()) {
173
            throw StatementError::new($this->stmt);
174
        }
175
176
        if (! $this->metadataFetched) {
177
            $meta = $this->stmt->result_metadata();
178
            if ($meta !== false) {
179
                $this->hasColumns = true;
180
181
                $fields = $meta->fetch_fields();
182
                assert(is_array($fields));
183
184
                $this->columnNames = array_map(static function (stdClass $field) : string {
185
                    return $field->name;
186
                }, $fields);
187
188
                $meta->free();
189
            } else {
190
                $this->hasColumns = false;
191
            }
192
193
            $this->metadataFetched = true;
194
        }
195
196
        if ($this->hasColumns) {
197
            // Store result of every execution which has it. Otherwise it will be impossible
198
            // to execute a new statement in case if the previous one has non-fetched rows
199
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
200
            $this->stmt->store_result();
201
202
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
203
            // it will have to allocate as much memory as it may be needed for the given column type
204
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
205
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
206
            //
207
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
208
            // previously called on the statement, the values are unbound making the statement unusable.
209
            //
210
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
211
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
212
            // to the length of the ones fetched during the previous execution.
213
            $this->rowBoundValues = array_fill(0, count($this->columnNames), null);
214
215
            $refs = [];
216
            foreach ($this->rowBoundValues as $key => &$value) {
217
                $refs[$key] =& $value;
218
            }
219
220
            if (! $this->stmt->bind_result(...$refs)) {
221
                throw StatementError::new($this->stmt);
222
            }
223
        }
224
225
        $this->result = true;
226
    }
227
228
    /**
229
     * Binds parameters with known types previously bound to the statement
230
     *
231
     * @throws DriverException
232
     */
233
    private function bindTypedParameters() : void
234
    {
235
        $streams = $values = [];
236
        $types   = $this->types;
237
238
        foreach ($this->boundValues as $parameter => $value) {
239
            assert(is_int($parameter));
240
            if (! isset($types[$parameter - 1])) {
241
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
242
            }
243
244
            if ($types[$parameter - 1] === self::$paramTypeMap[ParameterType::LARGE_OBJECT]) {
245
                if (is_resource($value)) {
246
                    if (get_resource_type($value) !== 'stream') {
247
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
248
                    }
249
250
                    $streams[$parameter] = $value;
251
                    $values[$parameter]  = null;
252
                    continue;
253
                }
254
255
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
256
            }
257
258
            $values[$parameter] = $value;
259
        }
260
261
        if (count($values) > 0 && ! $this->stmt->bind_param($types, ...$values)) {
262
            throw StatementError::new($this->stmt);
263
        }
264
265
        $this->sendLongData($streams);
266
    }
267
268
    /**
269
     * Handle $this->_longData after regular query parameters have been bound
270
     *
271
     * @param array<int, resource> $streams
272
     *
273
     * @throws MysqliException
274
     */
275
    private function sendLongData(array $streams) : void
276
    {
277
        foreach ($streams as $paramNr => $stream) {
278
            while (! feof($stream)) {
279
                $chunk = fread($stream, 8192);
280
281
                if ($chunk === false) {
282
                    throw FailedReadingStreamOffset::new($paramNr);
283
                }
284
285
                if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
286
                    throw StatementError::new($this->stmt);
287
                }
288
            }
289
        }
290
    }
291
292
    /**
293
     * Binds a array of values to bound parameters.
294
     *
295
     * @param mixed[] $values
296
     */
297
    private function bindUntypedValues(array $values) : bool
298
    {
299
        $params = [];
300
        $types  = str_repeat('s', count($values));
301
302
        foreach ($values as &$v) {
303
            $params[] =& $v;
304
        }
305
306
        return $this->stmt->bind_param($types, ...$params);
307
    }
308
309
    /**
310
     * @return mixed[]|false|null
311
     */
312
    private function _fetch()
313
    {
314
        $ret = $this->stmt->fetch();
315
316
        if ($ret === true) {
317
            $values = [];
318
            foreach ($this->rowBoundValues as $v) {
319
                $values[] = $v;
320
            }
321
322
            return $values;
323
        }
324
325
        return $ret;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331
    public function fetch(?int $fetchMode = null, ...$args)
332
    {
333
        // do not try fetching from the statement if it's not expected to contain result
334
        // in order to prevent exceptional situation
335
        if (! $this->result) {
336
            return false;
337
        }
338
339
        $fetchMode = $fetchMode ?? $this->defaultFetchMode;
340
341
        if ($fetchMode === FetchMode::COLUMN) {
342
            return $this->fetchColumn();
343
        }
344
345
        $values = $this->_fetch();
346
347
        if ($values === null) {
348
            return false;
349
        }
350
351
        if ($values === false) {
352
            throw StatementError::new($this->stmt);
353
        }
354
355
        if ($fetchMode === FetchMode::NUMERIC) {
356
            return $values;
357
        }
358
359
        $assoc = array_combine($this->columnNames, $values);
360
        assert(is_array($assoc));
361
362
        switch ($fetchMode) {
363
            case FetchMode::ASSOCIATIVE:
364
                return $assoc;
365
366
            case FetchMode::MIXED:
367
                return $assoc + $values;
368
369
            case FetchMode::STANDARD_OBJECT:
370
                return (object) $assoc;
371
372
            default:
373
                throw UnknownFetchMode::new($fetchMode);
374
        }
375
    }
376
377
    /**
378
     * {@inheritdoc}
379
     */
380
    public function fetchAll(?int $fetchMode = null, ...$args) : array
381
    {
382
        $fetchMode = $fetchMode ?? $this->defaultFetchMode;
383
384
        $rows = [];
385
386
        if ($fetchMode === FetchMode::COLUMN) {
387
            while (($row = $this->fetchColumn()) !== false) {
388
                $rows[] = $row;
389
            }
390
        } else {
391
            while (($row = $this->fetch($fetchMode)) !== false) {
392
                $rows[] = $row;
393
            }
394
        }
395
396
        return $rows;
397
    }
398
399
    /**
400
     * {@inheritdoc}
401
     */
402
    public function fetchColumn(int $columnIndex = 0)
403
    {
404
        $row = $this->fetch(FetchMode::NUMERIC);
405
406
        if ($row === false) {
407
            return false;
408
        }
409
410
        if (! array_key_exists($columnIndex, $row)) {
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type object; however, parameter $search of array_key_exists() 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

410
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
411
            throw InvalidColumnIndex::new($columnIndex, count($row));
412
        }
413
414
        return $row[$columnIndex];
415
    }
416
417
    public function closeCursor() : void
418
    {
419
        $this->stmt->free_result();
420
        $this->result = false;
421
    }
422
423
    public function rowCount() : int
424
    {
425
        if ($this->hasColumns) {
426
            return $this->stmt->num_rows;
427
        }
428
429
        return $this->stmt->affected_rows;
430
    }
431
432
    public function columnCount() : int
433
    {
434
        return $this->stmt->field_count;
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     */
440
    public function setFetchMode(int $fetchMode, ...$args) : void
441
    {
442
        $this->defaultFetchMode = $fetchMode;
443
    }
444
445
    /**
446
     * {@inheritdoc}
447
     */
448
    public function getIterator()
449
    {
450
        return new StatementIterator($this);
451
    }
452
}
453