Failed Conditions
Pull Request — master (#3217)
by Matthias
16:32
created

MysqliStatement::rowCount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.1481

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 2
cts 3
cp 0.6667
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2.1481
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
     * Populated by prepareLongData(), this array holds all
92
     * stream resources bound using the LARGE_OBJECT type.
93
     * These need to be sent using send_long_data *after*
94
     * bind_param has been called.
95
     *
96
     * @var string[]|resource[]
97
     */
98
    private $longData = [];
99
100
    /**
101
     * @var int
102
     */
103
    protected $_defaultFetchMode = FetchMode::MIXED;
104
105
    /**
106
     * Indicates whether the statement is in the state when fetching results is possible
107
     *
108
     * @var bool
109
     */
110
    private $result = false;
111
112
    /**
113
     * @param \mysqli $conn
114
     * @param string  $prepareString
115
     *
116
     * @throws \Doctrine\DBAL\Driver\Mysqli\MysqliException
117
     */
118 774
    public function __construct(\mysqli $conn, $prepareString)
119
    {
120 774
        $this->_conn = $conn;
121 774
        $this->_stmt = $conn->prepare($prepareString);
122 774
        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...
123 12
            throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno);
124
        }
125
126 762
        $paramCount = $this->_stmt->param_count;
127 762
        if (0 < $paramCount) {
128 375
            $this->types = str_repeat('s', $paramCount);
129 375
            $this->_bindedValues = array_fill(1, $paramCount, null);
130
        }
131 762
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 24
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
137
    {
138 24
        if (null === $type) {
139
            $type = 's';
140
        } else {
141 24
            if (isset(self::$_paramTypeMap[$type])) {
142 24
                $type = self::$_paramTypeMap[$type];
143
            } else {
144
                throw new MysqliException("Unknown type: '{$type}'");
145
            }
146
        }
147
148 24
        $this->_bindedValues[$column] =& $variable;
149 24
        $this->types[$column - 1] = $type;
150
151 24
        return true;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 108
    public function bindValue($param, $value, $type = ParameterType::STRING)
158
    {
159 108
        if (null === $type) {
160
            $type = 's';
161
        } else {
162 108
            if (isset(self::$_paramTypeMap[$type])) {
163 108
                $type = self::$_paramTypeMap[$type];
164
            } else {
165
                throw new MysqliException("Unknown type: '{$type}'");
166
            }
167
        }
168
169 108
        $this->_values[$param] = $value;
170 108
        $this->_bindedValues[$param] =& $this->_values[$param];
171 108
        $this->types[$param - 1] = $type;
172
173 108
        return true;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 738
    public function execute($params = null)
180
    {
181 738
        if (null !== $this->_bindedValues) {
182 375
            if (null !== $params) {
183 264
                if ( ! $this->_bindValues($params)) {
184 264
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
185
                }
186
            } else {
187 132
                $this->prepareLongData();
188 132
                if (! $this->_stmt->bind_param($this->types, ...$this->_bindedValues)) {
189
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
190
                }
191 132
                $this->processLongData();
192
            }
193
        }
194
195 738
        if ( ! $this->_stmt->execute()) {
196 18
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
197
        }
198
199 735
        if (null === $this->_columnNames) {
200 735
            $meta = $this->_stmt->result_metadata();
201 735
            if (false !== $meta) {
202 678
                $columnNames = [];
203 678
                foreach ($meta->fetch_fields() as $col) {
204 678
                    $columnNames[] = $col->name;
205
                }
206 678
                $meta->free();
207
208 678
                $this->_columnNames = $columnNames;
209
            } else {
210 295
                $this->_columnNames = false;
211
            }
212
        }
213
214 735
        if (false !== $this->_columnNames) {
215
            // Store result of every execution which has it. Otherwise it will be impossible
216
            // to execute a new statement in case if the previous one has non-fetched rows
217
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
218 678
            $this->_stmt->store_result();
219
220
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
221
            // it will have to allocate as much memory as it may be needed for the given column type
222
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
223
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
224
            //
225
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
226
            // previously called on the statement, the values are unbound making the statement unusable.
227
            //
228
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
229
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
230
            // to the length of the ones fetched during the previous execution.
231 678
            $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

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

374
                return array_combine(/** @scrutinizer ignore-type */ $this->_columnNames, $values);
Loading history...
375
376 9
            case FetchMode::MIXED:
377 6
                $ret = array_combine($this->_columnNames, $values);
378 6
                $ret += $values;
379
380 6
                return $ret;
381
382 3
            case FetchMode::STANDARD_OBJECT:
383 3
                $assoc = array_combine($this->_columnNames, $values);
384 3
                $ret = new \stdClass();
385
386 3
                foreach ($assoc as $column => $value) {
387 3
                    $ret->$column = $value;
388
                }
389
390 3
                return $ret;
391
392
            default:
393
                throw new MysqliException("Unknown fetch type '{$fetchMode}'");
394
        }
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
401
    {
402 336
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
403
404 336
        $rows = [];
405
406 336
        if ($fetchMode === FetchMode::COLUMN) {
407 27
            while (($row = $this->fetchColumn()) !== false) {
408 27
                $rows[] = $row;
409
            }
410
        } else {
411 309
            while (($row = $this->fetch($fetchMode)) !== false) {
412 288
                $rows[] = $row;
413
            }
414
        }
415
416 336
        return $rows;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function fetchColumn($columnIndex = 0)
423
    {
424 177
        $row = $this->fetch(FetchMode::NUMERIC);
425
426 177
        if (false === $row) {
427 45
            return false;
428
        }
429
430 159
        return $row[$columnIndex] ?? null;
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436
    public function errorCode()
437
    {
438
        return $this->_stmt->errno;
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444
    public function errorInfo()
445
    {
446
        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...
447
    }
448
449
    /**
450
     * {@inheritdoc}
451
     */
452
    public function closeCursor()
453
    {
454 60
        $this->_stmt->free_result();
455 60
        $this->result = false;
456
457 60
        return true;
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463
    public function rowCount()
464
    {
465 285
        if (false === $this->_columnNames) {
466 285
            return $this->_stmt->affected_rows;
467
        }
468
469
        return $this->_stmt->num_rows;
470
    }
471
472
    /**
473
     * {@inheritdoc}
474
     */
475
    public function columnCount()
476
    {
477 12
        return $this->_stmt->field_count;
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
484
    {
485 714
        $this->_defaultFetchMode = $fetchMode;
486
487 714
        return true;
488
    }
489
490
    /**
491
     * {@inheritdoc}
492
     */
493
    public function getIterator()
494
    {
495 41
        return new StatementIterator($this);
496
    }
497
}
498