Completed
Pull Request — master (#3070)
by Sergei
64:44
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 5.9256

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 12
ccs 6
cts 9
cp 0.6667
rs 9.6111
c 0
b 0
f 0
cc 5
nc 5
nop 1
crap 5.9256
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
    public function __construct(mysqli $conn, string $sql)
106
    {
107 3129
        $this->conn = $conn;
108
109 3129
        $stmt = $conn->prepare($sql);
110
111 3129
        if ($stmt === false) {
112
            throw ConnectionError::new($this->conn);
113 3129
        }
114 36
115
        $this->stmt = $stmt;
116
117 3093
        $paramCount = $this->stmt->param_count;
118
        if (0 >= $paramCount) {
119 3093
            return;
120 3093
        }
121 2490
122
        $this->types       = str_repeat('s', $paramCount);
123
        $this->boundValues = array_fill(1, $paramCount, null);
124 1332
    }
125 1332
126 1332
    /**
127
     * {@inheritdoc}
128
     */
129
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
130
    {
131 216
        assert(is_int($param));
132
133 216
        if (! isset(self::$paramTypeMap[$type])) {
134
            throw UnknownType::new($type);
135 216
        }
136
137
        $this->boundValues[$param] =& $variable;
138
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
139 216
    }
140 216
141 216
    /**
142
     * {@inheritdoc}
143
     */
144
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
145
    {
146 315
        assert(is_int($param));
147
148 315
        if (! isset(self::$paramTypeMap[$type])) {
149
            throw UnknownType::new($type);
150 315
        }
151
152
        $this->values[$param]      = $value;
153
        $this->boundValues[$param] =& $this->values[$param];
154 315
        $this->types[$param - 1]   = self::$paramTypeMap[$type];
155 315
    }
156 315
157 315
    /**
158
     * {@inheritdoc}
159
     */
160
    public function execute(?array $params = null) : void
161
    {
162 3030
        if ($params !== null && count($params) > 0) {
163
            if (! $this->bindUntypedValues($params)) {
164 3030
                throw StatementError::new($this->stmt);
165 864
            }
166 864
        } else {
167
            $this->bindTypedParameters();
168
        }
169 2778
170
        if (! $this->stmt->execute()) {
171
            throw StatementError::new($this->stmt);
172 3021
        }
173 54
174
        if (! $this->metadataFetched) {
175
            $meta = $this->stmt->result_metadata();
176 3012
            if ($meta !== false) {
177 3012
                $this->hasColumns = true;
178 3012
179 2841
                $fields = $meta->fetch_fields();
180
                assert(is_array($fields));
181 2841
182 2841
                $this->columnNames = array_map(static function (stdClass $field) : string {
183
                    return $field->name;
184
                }, $fields);
185 2841
186 2841
                $meta->free();
187
            } else {
188 2841
                $this->hasColumns = false;
189
            }
190 969
191
            $this->metadataFetched = true;
192
        }
193 3012
194
        if ($this->hasColumns) {
195
            // Store result of every execution which has it. Otherwise it will be impossible
196 3012
            // 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
            $this->stmt->store_result();
199
200 2841
            // 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
            $this->rowBoundValues = array_fill(0, count($this->columnNames), null);
212
213 2841
            $refs = [];
214
            foreach ($this->rowBoundValues as $key => &$value) {
215 2841
                $refs[$key] =& $value;
216 2841
            }
217 2841
218
            if (! $this->stmt->bind_result(...$refs)) {
219
                throw StatementError::new($this->stmt);
220 2841
            }
221
        }
222
223
        $this->result = true;
224
    }
225 3012
226 3012
    /**
227
     * Binds parameters with known types previously bound to the statement
228
     *
229
     * @throws DriverException
230
     */
231
    private function bindTypedParameters() : void
232
    {
233 2778
        $streams = $values = [];
234
        $types   = $this->types;
235 2778
236 2778
        foreach ($this->boundValues as $parameter => $value) {
237
            if (! isset($types[$parameter - 1])) {
238 2778
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
239 531
            }
240
241
            if ($types[$parameter - 1] === self::$paramTypeMap[ParameterType::LARGE_OBJECT]) {
242
                if (is_resource($value)) {
243 531
                    if (get_resource_type($value) !== 'stream') {
244 63
                        throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
245 27
                    }
246
247
                    $streams[$parameter] = $value;
248 27
                    $values[$parameter]  = null;
249 27
                    continue;
250 27
                }
251
252
                $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
253 45
            }
254
255
            $values[$parameter] = $value;
256 522
        }
257
258
        if (count($values) > 0 && ! $this->stmt->bind_param($types, ...$values)) {
259 2778
            throw StatementError::new($this->stmt);
260
        }
261
262
        $this->sendLongData($streams);
263 2778
    }
264 2778
265
    /**
266
     * Handle $this->_longData after regular query parameters have been bound
267
     *
268
     * @param resource[] $streams
269
     *
270
     * @throws MysqliException
271
     */
272
    private function sendLongData(array $streams) : void
273 2778
    {
274
        foreach ($streams as $paramNr => $stream) {
275 2778
            while (! feof($stream)) {
276 27
                $chunk = fread($stream, 8192);
277 27
278
                if ($chunk === false) {
279 27
                    throw FailedReadingStreamOffset::new($paramNr);
280
                }
281
282
                if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
283 27
                    throw StatementError::new($this->stmt);
284
                }
285
            }
286
        }
287
    }
288 2778
289
    /**
290
     * Binds a array of values to bound parameters.
291
     *
292
     * @param mixed[] $values
293
     */
294
    private function bindUntypedValues(array $values) : bool
295 864
    {
296
        $params = [];
297 864
        $types  = str_repeat('s', count($values));
298 864
299
        foreach ($values as &$v) {
300 864
            $params[] =& $v;
301 864
        }
302
303
        return $this->stmt->bind_param($types, ...$params);
304 864
    }
305
306
    /**
307
     * @return mixed[]|false|null
308
     */
309
    private function _fetch()
310 2769
    {
311
        $ret = $this->stmt->fetch();
312 2769
313
        if ($ret === true) {
314 2769
            $values = [];
315 2670
            foreach ($this->rowBoundValues as $v) {
316 2670
                $values[] = $v;
317 2670
            }
318
319
            return $values;
320 2670
        }
321
322
        return $ret;
323 1302
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328
    public function fetch(?int $fetchMode = null)
329 2850
    {
330
        // do not try fetching from the statement if it's not expected to contain result
331
        // in order to prevent exceptional situation
332
        if (! $this->result) {
333 2850
            return false;
334 81
        }
335
336
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
337 2769
338
        if ($fetchMode === FetchMode::COLUMN) {
339 2769
            return $this->fetchColumn();
340 9
        }
341
342
        $values = $this->_fetch();
343 2769
344
        if ($values === null) {
345 2769
            return false;
346 1302
        }
347
348
        if ($values === false) {
349 2670
            throw StatementError::new($this->stmt);
350
        }
351
352
        if ($fetchMode === FetchMode::NUMERIC) {
353 2670
            return $values;
354 1878
        }
355
356
        $assoc = array_combine($this->columnNames, $values);
357 1491
        assert(is_array($assoc));
358 1491
359
        switch ($fetchMode) {
360 1491
            case FetchMode::ASSOCIATIVE:
361
                return $assoc;
362 1464
363
            case FetchMode::MIXED:
364
                return $assoc + $values;
365 18
366
            default:
367
                throw UnknownFetchMode::new($fetchMode);
368 9
        }
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374
    public function fetchAll(?int $fetchMode = null) : array
375
    {
376
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
377
378 1176
        $rows = [];
379
380 1176
        if ($fetchMode === FetchMode::COLUMN) {
381
            while (($row = $this->fetchColumn()) !== false) {
382 1176
                $rows[] = $row;
383
            }
384 1176
        } else {
385 90
            while (($row = $this->fetch($fetchMode)) !== false) {
386 90
                $rows[] = $row;
387
            }
388
        }
389 1086
390 1005
        return $rows;
391
    }
392
393
    /**
394 1176
     * {@inheritdoc}
395
     */
396
    public function fetchColumn()
397
    {
398
        $row = $this->fetch(FetchMode::NUMERIC);
399
400 1869
        if ($row === false) {
401
            return false;
402 1869
        }
403
404 1869
        return $row[0];
405 144
    }
406
407
    public function closeCursor() : void
408 1815
    {
409 18
        $this->stmt->free_result();
410
        $this->result = false;
411
    }
412 1797
413
    public function rowCount() : int
414
    {
415
        if ($this->hasColumns) {
416
            return $this->stmt->num_rows;
417
        }
418 171
419
        return $this->stmt->affected_rows;
420 171
    }
421 171
422 171
    public function columnCount() : int
423
    {
424
        return $this->stmt->field_count;
425
    }
426
427 927
    /**
428
     * {@inheritdoc}
429 927
     */
430
    public function setFetchMode(int $fetchMode, ...$args) : void
431
    {
432
        $this->defaultFetchMode = $fetchMode;
433 927
    }
434
435
    /**
436
     * {@inheritdoc}
437
     */
438
    public function getIterator()
439 36
    {
440
        return new StatementIterator($this);
441 36
    }
442
}
443