Failed Conditions
Pull Request — master (#4007)
by Sergei
11:47
created

MysqliStatement::bindParam()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.2109

Importance

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