Passed
Pull Request — master (#3070)
by Sergei
07:45
created

MysqliStatement::sendLongData()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.6

Importance

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