Failed Conditions
Pull Request — master (#3217)
by Matthias
64:02
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 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Driver\Mysqli;
21
22
use Doctrine\DBAL\Driver\Statement;
23
use Doctrine\DBAL\Driver\StatementIterator;
24
use Doctrine\DBAL\Exception\InvalidArgumentException;
25
use Doctrine\DBAL\FetchMode;
26
use Doctrine\DBAL\ParameterType;
27
use function array_combine;
28
use function array_fill;
29
use function count;
30
use function feof;
31
use function fread;
32
use function get_resource_type;
33
use function is_resource;
34
use function str_repeat;
35
36
/**
37
 * @author Kim Hemsø Rasmussen <[email protected]>
38
 */
39
class MysqliStatement implements \IteratorAggregate, Statement
40
{
41
    /**
42
     * @var array
43
     */
44
    protected static $_paramTypeMap = [
45
        ParameterType::STRING       => 's',
46
        ParameterType::BINARY       => 's',
47
        ParameterType::BOOLEAN      => 'i',
48
        ParameterType::NULL         => 's',
49
        ParameterType::INTEGER      => 'i',
50
        ParameterType::LARGE_OBJECT => 'b',
51
    ];
52
53
    /**
54
     * @var \mysqli
55
     */
56
    protected $_conn;
57
58
    /**
59
     * @var \mysqli_stmt
60
     */
61
    protected $_stmt;
62
63
    /**
64
     * @var null|boolean|array
65
     */
66
    protected $_columnNames;
67
68
    /**
69
     * @var null|array
70
     */
71
    protected $_rowBindedValues;
72
73
    /**
74
     * @var array
75
     */
76
    protected $_bindedValues;
77
78
    /**
79
     * @var string
80
     */
81
    protected $types;
82
83
    /**
84
     * Contains ref values for bindValue().
85
     *
86
     * @var array
87
     */
88
    protected $_values = [];
89
90
    /**
91
     * Contains values from bindValue() that need to be sent
92
     * using send_long_data *after* bind_param has been called.
93
     *
94
     * @var string[]|resource[]
95
     */
96
    protected $longData = [];
97
98
    /**
99
     * @var int
100
     */
101
    protected $_defaultFetchMode = FetchMode::MIXED;
102
103 765
    /**
104
     * Indicates whether the statement is in the state when fetching results is possible
105 765
     *
106 765
     * @var bool
107 765
     */
108 12
    private $result = false;
109
110
    /**
111 753
     * @param \mysqli $conn
112 753
     * @param string  $prepareString
113 366
     *
114 366
     * @throws \Doctrine\DBAL\Driver\Mysqli\MysqliException
115
     */
116 753
    public function __construct(\mysqli $conn, $prepareString)
117
    {
118
        $this->_conn = $conn;
119
        $this->_stmt = $conn->prepare($prepareString);
120
        if (false === $this->_stmt) {
0 ignored issues
show
introduced by
The condition $this->_stmt is always false. If $this->_stmt can have other possible types, add them to lib/Doctrine/DBAL/Driver...qli/MysqliStatement.php:59
Loading history...
121 21
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
122
        }
123 21
124
        $paramCount = $this->_stmt->param_count;
125
        if (0 < $paramCount) {
126 21
            $this->types = str_repeat('s', $paramCount);
127 21
            $this->_bindedValues = array_fill(1, $paramCount, null);
128
        }
129
    }
130
131
    /**
132
     * {@inheritdoc}
133 21
     */
134 21
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
135
    {
136 21
        if (null === $type) {
137
            $type = 's';
138
        } else {
139
            if (isset(self::$_paramTypeMap[$type])) {
140
                $type = self::$_paramTypeMap[$type];
141
            } else {
142 102
                throw new MysqliException("Unknown type: '{$type}'");
143
            }
144 102
        }
145
146
        $this->_bindedValues[$column] =& $variable;
147 102
        $this->types[$column - 1] = $type;
148 102
149
        return true;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154 102
     */
155 102
    public function bindValue($param, $value, $type = ParameterType::STRING)
156 102
    {
157
        if ($type === ParameterType::LARGE_OBJECT) {
158 102
            $this->longData[$param - 1] = $value;
159
            $value                      = null;
160
        }
161
162
        if (null === $type) {
163
            $type = 's';
164 729
        } else {
165
            if (isset(self::$_paramTypeMap[$type])) {
166 729
                $type = self::$_paramTypeMap[$type];
167 366
            } else {
168 264
                throw new MysqliException("Unknown type: '{$type}'");
169 264
            }
170
        }
171
172 123
        $this->_values[$param] = $value;
173
        $this->_bindedValues[$param] =& $this->_values[$param];
174
        $this->types[$param - 1] = $type;
175
176
        return true;
177
    }
178 729
179 18
    /**
180
     * {@inheritdoc}
181
     */
182 726
    public function execute($params = null)
183 726
    {
184 726
        if (null !== $this->_bindedValues) {
185 669
            if (null !== $params) {
186 669
                if ( ! $this->_bindValues($params)) {
187 669
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
188
                }
189 669
            } else {
190
                if (! $this->_stmt->bind_param($this->types, ...$this->_bindedValues)) {
191 669
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
192
                }
193 286
                $this->processLongData();
194
            }
195
        }
196
197 726
        if ( ! $this->_stmt->execute()) {
198
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
199
        }
200
201 669
        if (null === $this->_columnNames) {
202
            $meta = $this->_stmt->result_metadata();
203
            if (false !== $meta) {
204
                $columnNames = [];
205
                foreach ($meta->fetch_fields() as $col) {
206
                    $columnNames[] = $col->name;
207
                }
208
                $meta->free();
209
210
                $this->_columnNames = $columnNames;
211
            } else {
212
                $this->_columnNames = false;
213
            }
214 669
        }
215
216 669
        if (false !== $this->_columnNames) {
217 669
            // Store result of every execution which has it. Otherwise it will be impossible
218 669
            // to execute a new statement in case if the previous one has non-fetched rows
219
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
220
            $this->_stmt->store_result();
221 669
222
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
223
            // it will have to allocate as much memory as it may be needed for the given column type
224
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
225
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
226 726
            //
227
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
228 726
            // previously called on the statement, the values are unbound making the statement unusable.
229
            //
230
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
231
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
232
            // to the length of the ones fetched during the previous execution.
233
            $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

233
            $this->_rowBindedValues = array_fill(0, count(/** @scrutinizer ignore-type */ $this->_columnNames), null);
Loading history...
234
235
            $refs = [];
236
            foreach ($this->_rowBindedValues as $key => &$value) {
237
                $refs[$key] =& $value;
238 264
            }
239
240 264
            if (! $this->_stmt->bind_result(...$refs)) {
241 264
                throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
242
            }
243 264
        }
244 264
245
        $this->result = true;
246
247 264
        return true;
248
    }
249
250
    /**
251
     * Handle $this->_longData after regular query parameters have been bound
252
     *
253 645
     * @throws MysqliException
254
     */
255 645
    private function processLongData()
256
    {
257 645
        foreach ($this->longData as $paramNr => $value) {
258 612
            if (! is_resource($value)) {
259 612
                $this->sendLongData($paramNr, $value);
260 612
261
                continue;
262
            }
263 612
264
            if (get_resource_type($value) !== 'stream') {
265
                throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
266 372
            }
267
268
            $stream = $value;
269
            while (! feof($stream)) {
270
                $chunk = fread($stream, 8192);
271
                if ($chunk === false) {
272 672
                    throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
273
                }
274
                $this->sendLongData($paramNr, $chunk);
275
            }
276 672
        }
277 27
    }
278
279
    /**
280 645
     * Bind parameters using send_long_data
281
     *
282 645
     * @param int    $paramNr  Parameter offset
283 3
     * @param string $longData A chunk of data to send
284
     *
285
     * @throws MysqliException
286 645
     */
287 645
    private function sendLongData($paramNr, $longData) : void
288 372
    {
289
        if (! $this->_stmt->send_long_data($paramNr, $longData)) {
290
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
291 612
        }
292
    }
293
294
    /**
295
     * Binds a array of values to bound parameters.
296 612
     *
297 162
     * @param array $values
298
     *
299 456
     * @return bool
300 447
     */
301
    private function _bindValues($values)
302 9
    {
303 6
        $params = [];
304 6
        $types = str_repeat('s', count($values));
305
306 6
        foreach ($values as &$v) {
307
            $params[] =& $v;
308 3
        }
309 3
310 3
        return $this->_stmt->bind_param($types, ...$params);
311
    }
312 3
313 3
    /**
314
     * @return mixed[]|false
315
     */
316 3
    private function _fetch()
317
    {
318
        $ret = $this->_stmt->fetch();
319
320
        if (true === $ret) {
321
            $values = [];
322
            foreach ($this->_rowBindedValues as $v) {
323
                $values[] = $v;
324
            }
325
326 327
            return $values;
327
        }
328 327
329
        return $ret;
330 327
    }
331
332 327
    /**
333 12
     * {@inheritdoc}
334 12
     */
335
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
336
    {
337 315
        // do not try fetching from the statement if it's not expected to contain result
338 294
        // in order to prevent exceptional situation
339
        if (!$this->result) {
340
            return false;
341
        }
342 327
343
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
344
345
        if ($fetchMode === FetchMode::COLUMN) {
346
            return $this->fetchColumn();
347
        }
348 162
349
        $values = $this->_fetch();
350 162
        if (null === $values) {
0 ignored issues
show
introduced by
The condition null === $values is always false.
Loading history...
351
            return false;
352 162
        }
353 30
354
        if (false === $values) {
355
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
356 144
        }
357
358
        switch ($fetchMode) {
359
            case FetchMode::NUMERIC:
360
                return $values;
361
362
            case FetchMode::ASSOCIATIVE:
363
                return array_combine($this->_columnNames, $values);
0 ignored issues
show
Bug introduced by
It seems like $this->_columnNames can also be of type null and boolean; however, parameter $keys of array_combine() 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

363
                return array_combine(/** @scrutinizer ignore-type */ $this->_columnNames, $values);
Loading history...
364
365
            case FetchMode::MIXED:
366
                $ret = array_combine($this->_columnNames, $values);
367
                $ret += $values;
368
369
                return $ret;
370
371
            case FetchMode::STANDARD_OBJECT:
372
                $assoc = array_combine($this->_columnNames, $values);
373
                $ret = new \stdClass();
374
375
                foreach ($assoc as $column => $value) {
376
                    $ret->$column = $value;
377
                }
378 60
379
                return $ret;
380 60
381 60
            default:
382
                throw new MysqliException("Unknown fetch type '{$fetchMode}'");
383 60
        }
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     */
389 279
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
390
    {
391 279
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
392 279
393
        $rows = [];
394
395
        if ($fetchMode === FetchMode::COLUMN) {
396
            while (($row = $this->fetchColumn()) !== false) {
397
                $rows[] = $row;
398
            }
399
        } else {
400
            while (($row = $this->fetch($fetchMode)) !== false) {
401 12
                $rows[] = $row;
402
            }
403 12
        }
404
405
        return $rows;
406
    }
407
408
    /**
409 705
     * {@inheritdoc}
410
     */
411 705
    public function fetchColumn($columnIndex = 0)
412
    {
413 705
        $row = $this->fetch(FetchMode::NUMERIC);
414
415
        if (false === $row) {
416
            return false;
417
        }
418
419 41
        return $row[$columnIndex] ?? null;
420
    }
421 41
422
    /**
423
     * {@inheritdoc}
424
     */
425
    public function errorCode()
426
    {
427
        return $this->_stmt->errno;
428
    }
429
430
    /**
431
     * {@inheritdoc}
432
     */
433
    public function errorInfo()
434
    {
435
        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.

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...
436
    }
437
438
    /**
439
     * {@inheritdoc}
440
     */
441
    public function closeCursor()
442
    {
443
        $this->_stmt->free_result();
444
        $this->result = false;
445
446
        return true;
447
    }
448
449
    /**
450
     * {@inheritdoc}
451
     */
452
    public function rowCount()
453
    {
454
        if (false === $this->_columnNames) {
455
            return $this->_stmt->affected_rows;
456
        }
457
458
        return $this->_stmt->num_rows;
459
    }
460
461
    /**
462
     * {@inheritdoc}
463
     */
464
    public function columnCount()
465
    {
466
        return $this->_stmt->field_count;
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     */
472
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
473
    {
474
        $this->_defaultFetchMode = $fetchMode;
475
476
        return true;
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     */
482
    public function getIterator()
483
    {
484
        return new StatementIterator($this);
485
    }
486
}
487