Failed Conditions
Pull Request — master (#3217)
by Matthias
60:50
created

MysqliStatement::processLongData()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 20
ccs 6
cts 6
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 5
nop 0
crap 6
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
    private $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 ($type === ParameterType::LARGE_OBJECT) {
137
            $this->longData[$column - 1]  =& $variable;
138
            $null                         = null;
139
            $variable                     =& $null;
140
        }
141
142 102
        if (null === $type) {
143
            $type = 's';
144 102
        } else {
145
            if (isset(self::$_paramTypeMap[$type])) {
146
                $type = self::$_paramTypeMap[$type];
147 102
            } else {
148 102
                throw new MysqliException("Unknown type: '{$type}'");
149
            }
150
        }
151
152
        $this->_bindedValues[$column] =& $variable;
153
        $this->types[$column - 1] = $type;
154 102
155 102
        return true;
156 102
    }
157
158 102
    /**
159
     * {@inheritdoc}
160
     */
161
    public function bindValue($param, $value, $type = ParameterType::STRING)
162
    {
163
        if ($type === ParameterType::LARGE_OBJECT) {
164 729
            $this->longData[$param - 1] = $value;
165
            $value                      = null;
166 729
        }
167 366
168 264
        if (null === $type) {
169 264
            $type = 's';
170
        } else {
171
            if (isset(self::$_paramTypeMap[$type])) {
172 123
                $type = self::$_paramTypeMap[$type];
173
            } else {
174
                throw new MysqliException("Unknown type: '{$type}'");
175
            }
176
        }
177
178 729
        $this->_values[$param] = $value;
179 18
        $this->_bindedValues[$param] =& $this->_values[$param];
180
        $this->types[$param - 1] = $type;
181
182 726
        return true;
183 726
    }
184 726
185 669
    /**
186 669
     * {@inheritdoc}
187 669
     */
188
    public function execute($params = null)
189 669
    {
190
        if (null !== $this->_bindedValues) {
191 669
            if (null !== $params) {
192
                if ( ! $this->_bindValues($params)) {
193 286
                    throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
194
                }
195
            } else {
196
                if (! $this->_stmt->bind_param($this->types, ...$this->_bindedValues)) {
197 726
                    throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
198
                }
199
                $this->processLongData();
200
            }
201 669
        }
202
203
        if ( ! $this->_stmt->execute()) {
204
            throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
205
        }
206
207
        if (null === $this->_columnNames) {
208
            $meta = $this->_stmt->result_metadata();
209
            if (false !== $meta) {
210
                $columnNames = [];
211
                foreach ($meta->fetch_fields() as $col) {
212
                    $columnNames[] = $col->name;
213
                }
214 669
                $meta->free();
215
216 669
                $this->_columnNames = $columnNames;
217 669
            } else {
218 669
                $this->_columnNames = false;
219
            }
220
        }
221 669
222
        if (false !== $this->_columnNames) {
223
            // Store result of every execution which has it. Otherwise it will be impossible
224
            // to execute a new statement in case if the previous one has non-fetched rows
225
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
226 726
            $this->_stmt->store_result();
227
228 726
            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
229
            // it will have to allocate as much memory as it may be needed for the given column type
230
            // (e.g. for a LONGBLOB field it's 4 gigabytes)
231
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
232
            //
233
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
234
            // previously called on the statement, the values are unbound making the statement unusable.
235
            //
236
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
237
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
238 264
            // to the length of the ones fetched during the previous execution.
239
            $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

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

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