Failed Conditions
Push — develop ( 152bc9...e39bc0 )
by Sergei
102:42 queued 37:39
created

MysqliStatement::columnCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405
    public function closeCursor()
406
    {
407
        $this->_stmt->free_result();
408
        $this->result = false;
409
410
        return true;
411
    }
412 114
413
    /**
414 114
     * {@inheritdoc}
415 114
     */
416
    public function rowCount() : int
417 114
    {
418
        if ($this->_columnNames === false) {
419
            return $this->_stmt->affected_rows;
420
        }
421
422
        return $this->_stmt->num_rows;
423 570
    }
424
425 570
    /**
426 570
     * {@inheritdoc}
427
     */
428
    public function columnCount()
429
    {
430
        return $this->_stmt->field_count;
431
    }
432
433
    /**
434
     * {@inheritdoc}
435 24
     */
436
    public function setFetchMode($fetchMode, ...$args)
437 24
    {
438
        $this->_defaultFetchMode = $fetchMode;
439
440
        return true;
441
    }
442
443 1455
    /**
444
     * {@inheritdoc}
445 1455
     */
446
    public function getIterator()
447 1455
    {
448
        return new StatementIterator($this);
449
    }
450
}
451