Passed
Push — phpstan-tests ( 6a2b78...974220 )
by Michael
61:35 queued 23s
created

MysqliStatement::sendLongData()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

188
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
189
190
            $refs = [];
191
            foreach ($this->_rowBindedValues as $key => &$value) {
192
                $refs[$key] =& $value;
193
            }
194
195
            if (! $this->_stmt->bind_result(...$refs)) {
196
                throw MysqliException::fromStatementError($this->_stmt);
197
            }
198
        }
199
200
        $this->result = true;
201
    }
202
203
    /**
204
     * Binds parameters with known types previously bound to the statement
205
     *
206
     * @throws DriverException
207
     */
208
    private function bindTypedParameters()
209
    {
210
        $streams = $values = [];
211
        $types   = $this->types;
212
213
        foreach ($this->_bindedValues as $parameter => $value) {
214
            if (! isset($types[$parameter - 1])) {
215
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
216
            }
217
218
            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
219
                if (is_resource($value)) {
220
                    if (get_resource_type($value) !== 'stream') {
221
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
222
                    }
223
                    $streams[$parameter] = $value;
224
                    $values[$parameter]  = null;
225
                    continue;
226
                }
227
228
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
229
            }
230
231
            $values[$parameter] = $value;
232
        }
233
234
        if (count($values) > 0 && ! $this->_stmt->bind_param($types, ...$values)) {
235
            throw MysqliException::fromStatementError($this->_stmt);
236
        }
237
238
        $this->sendLongData($streams);
239
    }
240
241
    /**
242
     * Handle $this->_longData after regular query parameters have been bound
243
     *
244
     * @throws MysqliException
245
     */
246
    private function sendLongData($streams)
247
    {
248
        foreach ($streams as $paramNr => $stream) {
249
            while (! feof($stream)) {
250
                $chunk = fread($stream, 8192);
251
252
                if ($chunk === false) {
253
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
254
                }
255
256
                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
257
                    throw MysqliException::fromStatementError($this->_stmt);
258
                }
259
            }
260
        }
261
    }
262
263
    /**
264
     * Binds a array of values to bound parameters.
265
     *
266
     * @param mixed[] $values
267
     *
268
     * @return bool
269
     */
270
    private function bindUntypedValues(array $values)
271
    {
272
        $params = [];
273
        $types  = str_repeat('s', count($values));
274
275
        foreach ($values as &$v) {
276
            $params[] =& $v;
277
        }
278
279
        return $this->_stmt->bind_param($types, ...$params);
280
    }
281
282
    /**
283
     * @return mixed[]|false|null
284
     */
285
    private function _fetch()
286
    {
287
        $ret = $this->_stmt->fetch();
288
289
        if ($ret === true) {
290
            $values = [];
291
            foreach ($this->_rowBindedValues as $v) {
292
                $values[] = $v;
293
            }
294
295
            return $values;
296
        }
297
298
        return $ret;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function fetch($fetchMode = null, ...$args)
305
    {
306
        // do not try fetching from the statement if it's not expected to contain result
307
        // in order to prevent exceptional situation
308
        if (! $this->result) {
309
            return false;
310
        }
311
312
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
313
314
        if ($fetchMode === FetchMode::COLUMN) {
315
            return $this->fetchColumn();
316
        }
317
318
        $values = $this->_fetch();
319
320
        if ($values === null) {
321
            return false;
322
        }
323
324
        if ($values === false) {
325
            throw MysqliException::fromStatementError($this->_stmt);
326
        }
327
328
        if ($fetchMode === FetchMode::NUMERIC) {
329
            return $values;
330
        }
331
332
        assert(is_array($this->_columnNames));
333
        $assoc = array_combine($this->_columnNames, $values);
334
        assert(is_array($assoc));
335
336
        switch ($fetchMode) {
337
            case FetchMode::ASSOCIATIVE:
338
                return $assoc;
339
340
            case FetchMode::MIXED:
341
                return $assoc + $values;
342
343
            case FetchMode::STANDARD_OBJECT:
344
                return (object) $assoc;
345
346
            default:
347
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
348
        }
349
    }
350
351
    /**
352
     * {@inheritdoc}
353
     */
354
    public function fetchAll($fetchMode = null, ...$args)
355
    {
356
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
357
358
        $rows = [];
359
360
        if ($fetchMode === FetchMode::COLUMN) {
361
            while (($row = $this->fetchColumn()) !== false) {
362
                $rows[] = $row;
363
            }
364
        } else {
365
            while (($row = $this->fetch($fetchMode)) !== false) {
366
                $rows[] = $row;
367
            }
368
        }
369
370
        return $rows;
371
    }
372
373
    /**
374
     * {@inheritdoc}
375
     */
376
    public function fetchColumn($columnIndex = 0)
377
    {
378
        $row = $this->fetch(FetchMode::NUMERIC);
379
380
        if ($row === false) {
381
            return false;
382
        }
383
384
        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

384
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
385
            throw DBALException::invalidColumnIndex($columnIndex, count($row));
386
        }
387
388
        return $row[$columnIndex];
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394
    public function closeCursor() : void
395
    {
396
        $this->_stmt->free_result();
397
        $this->result = false;
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403
    public function rowCount() : int
404
    {
405
        if ($this->_columnNames === false) {
406
            return $this->_stmt->affected_rows;
407
        }
408
409
        return $this->_stmt->num_rows;
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415
    public function columnCount()
416
    {
417
        return $this->_stmt->field_count;
418
    }
419
420
    /**
421
     * {@inheritdoc}
422
     */
423
    public function setFetchMode($fetchMode, ...$args) : void
424
    {
425
        $this->_defaultFetchMode = $fetchMode;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431
    public function getIterator()
432
    {
433
        return new StatementIterator($this);
434
    }
435
}
436