1<?php
2
3namespace Doctrine\DBAL\Driver\Mysqli;
4
5use Doctrine\DBAL\Driver\FetchUtils;
6use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
7use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
8use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
9use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownType;
10use Doctrine\DBAL\Driver\Result;
11use Doctrine\DBAL\Driver\Statement as StatementInterface;
12use Doctrine\DBAL\Driver\StatementIterator;
13use Doctrine\DBAL\Exception\InvalidArgumentException;
14use Doctrine\DBAL\FetchMode;
15use Doctrine\DBAL\ParameterType;
16use IteratorAggregate;
17use mysqli;
18use mysqli_sql_exception;
19use mysqli_stmt;
20use PDO;
21use ReturnTypeWillChange;
22
23use function array_combine;
24use function array_fill;
25use function assert;
26use function count;
27use function feof;
28use function fread;
29use function get_resource_type;
30use function is_array;
31use function is_int;
32use function is_resource;
33use function sprintf;
34use function str_repeat;
35
36/**
37 * @deprecated Use {@link Statement} instead
38 */
39class MysqliStatement implements IteratorAggregate, StatementInterface, Result
40{
41    /** @var string[] */
42    protected static $_paramTypeMap = [
43        ParameterType::ASCII        => 's',
44        ParameterType::STRING       => 's',
45        ParameterType::BINARY       => 's',
46        ParameterType::BOOLEAN      => 'i',
47        ParameterType::NULL         => 's',
48        ParameterType::INTEGER      => 'i',
49        ParameterType::LARGE_OBJECT => 'b',
50    ];
51
52    /** @var mysqli */
53    protected $_conn;
54
55    /** @var mysqli_stmt */
56    protected $_stmt;
57
58    /** @var string[]|false|null */
59    protected $_columnNames;
60
61    /** @var mixed[] */
62    protected $_rowBindedValues = [];
63
64    /** @var mixed[] */
65    protected $_bindedValues;
66
67    /** @var string */
68    protected $types;
69
70    /**
71     * Contains ref values for bindValue().
72     *
73     * @var mixed[]
74     */
75    protected $_values = [];
76
77    /** @var int */
78    protected $_defaultFetchMode = FetchMode::MIXED;
79
80    /**
81     * Indicates whether the statement is in the state when fetching results is possible
82     *
83     * @var bool
84     */
85    private $result = false;
86
87    /**
88     * @internal The statement can be only instantiated by its driver connection.
89     *
90     * @param string $prepareString
91     *
92     * @throws MysqliException
93     */
94    public function __construct(mysqli $conn, $prepareString)
95    {
96        $this->_conn = $conn;
97
98        try {
99            $stmt = $conn->prepare($prepareString);
100        } catch (mysqli_sql_exception $e) {
101            throw ConnectionError::upcast($e);
102        }
103
104        if ($stmt === false) {
105            throw ConnectionError::new($this->_conn);
106        }
107
108        $this->_stmt = $stmt;
109
110        $paramCount          = $this->_stmt->param_count;
111        $this->types         = str_repeat('s', $paramCount);
112        $this->_bindedValues = array_fill(1, $paramCount, null);
113    }
114
115    /**
116     * {@inheritdoc}
117     */
118    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
119    {
120        assert(is_int($param));
121
122        if (! isset(self::$_paramTypeMap[$type])) {
123            throw UnknownType::new($type);
124        }
125
126        $this->_bindedValues[$param] =& $variable;
127        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
128
129        return true;
130    }
131
132    /**
133     * {@inheritdoc}
134     */
135    public function bindValue($param, $value, $type = ParameterType::STRING)
136    {
137        assert(is_int($param));
138
139        if (! isset(self::$_paramTypeMap[$type])) {
140            throw UnknownType::new($type);
141        }
142
143        $this->_values[$param]       = $value;
144        $this->_bindedValues[$param] =& $this->_values[$param];
145        $this->types[$param - 1]     = self::$_paramTypeMap[$type];
146
147        return true;
148    }
149
150    /**
151     * {@inheritdoc}
152     */
153    public function execute($params = null)
154    {
155        if ($params !== null && count($params) > 0) {
156            if (! $this->bindUntypedValues($params)) {
157                throw StatementError::new($this->_stmt);
158            }
159        } elseif (count($this->_bindedValues) > 0) {
160            $this->bindTypedParameters();
161        }
162
163        try {
164            $result = $this->_stmt->execute();
165        } catch (mysqli_sql_exception $e) {
166            throw StatementError::upcast($e);
167        }
168
169        if (! $result) {
170            throw StatementError::new($this->_stmt);
171        }
172
173        if ($this->_columnNames === null) {
174            $meta = $this->_stmt->result_metadata();
175            if ($meta !== false) {
176                $fields = $meta->fetch_fields();
177                assert(is_array($fields));
178
179                $columnNames = [];
180                foreach ($fields as $col) {
181                    $columnNames[] = $col->name;
182                }
183
184                $meta->free();
185
186                $this->_columnNames = $columnNames;
187            } else {
188                $this->_columnNames = false;
189            }
190        }
191
192        if ($this->_columnNames !== false) {
193            // Store result of every execution which has it. Otherwise it will be impossible
194            // to execute a new statement in case if the previous one has non-fetched rows
195            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
196            $this->_stmt->store_result();
197
198            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
199            // it will have to allocate as much memory as it may be needed for the given column type
200            // (e.g. for a LONGBLOB column it's 4 gigabytes)
201            // @link https://bugs.php.net/bug.php?id=51386#1270673122
202            //
203            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
204            // previously called on the statement, the values are unbound making the statement unusable.
205            //
206            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
207            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
208            // to the length of the ones fetched during the previous execution.
209            $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);
210
211            $refs = [];
212            foreach ($this->_rowBindedValues as $key => &$value) {
213                $refs[$key] =& $value;
214            }
215
216            if (! $this->_stmt->bind_result(...$refs)) {
217                throw StatementError::new($this->_stmt);
218            }
219        }
220
221        $this->result = true;
222
223        return true;
224    }
225
226    /**
227     * Binds parameters with known types previously bound to the statement
228     */
229    private function bindTypedParameters(): void
230    {
231        $streams = $values = [];
232        $types   = $this->types;
233
234        foreach ($this->_bindedValues as $parameter => $value) {
235            assert(is_int($parameter));
236
237            if (! isset($types[$parameter - 1])) {
238                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
239            }
240
241            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
242                if (is_resource($value)) {
243                    if (get_resource_type($value) !== 'stream') {
244                        throw new InvalidArgumentException(
245                            'Resources passed with the LARGE_OBJECT parameter type must be stream resources.'
246                        );
247                    }
248
249                    $streams[$parameter] = $value;
250                    $values[$parameter]  = null;
251                    continue;
252                }
253
254                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
255            }
256
257            $values[$parameter] = $value;
258        }
259
260        if (! $this->_stmt->bind_param($types, ...$values)) {
261            throw StatementError::new($this->_stmt);
262        }
263
264        $this->sendLongData($streams);
265    }
266
267    /**
268     * Handle $this->_longData after regular query parameters have been bound
269     *
270     * @param array<int, resource> $streams
271     *
272     * @throws MysqliException
273     */
274    private function sendLongData(array $streams): void
275    {
276        foreach ($streams as $paramNr => $stream) {
277            while (! feof($stream)) {
278                $chunk = fread($stream, 8192);
279
280                if ($chunk === false) {
281                    throw FailedReadingStreamOffset::new($paramNr);
282                }
283
284                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
285                    throw StatementError::new($this->_stmt);
286                }
287            }
288        }
289    }
290
291    /**
292     * Binds a array of values to bound parameters.
293     *
294     * @param mixed[] $values
295     *
296     * @return bool
297     */
298    private function bindUntypedValues(array $values)
299    {
300        $params = [];
301        $types  = str_repeat('s', count($values));
302
303        foreach ($values as &$v) {
304            $params[] =& $v;
305        }
306
307        return $this->_stmt->bind_param($types, ...$params);
308    }
309
310    /**
311     * @return mixed[]|false|null
312     *
313     * @throws StatementError
314     */
315    private function _fetch()
316    {
317        try {
318            $ret = $this->_stmt->fetch();
319        } catch (mysqli_sql_exception $e) {
320            throw StatementError::upcast($e);
321        }
322
323        if ($ret === true) {
324            $values = [];
325            foreach ($this->_rowBindedValues as $v) {
326                $values[] = $v;
327            }
328
329            return $values;
330        }
331
332        return $ret;
333    }
334
335    /**
336     * {@inheritdoc}
337     *
338     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
339     */
340    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
341    {
342        // do not try fetching from the statement if it's not expected to contain result
343        // in order to prevent exceptional situation
344        if (! $this->result) {
345            return false;
346        }
347
348        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
349
350        if ($fetchMode === FetchMode::COLUMN) {
351            return $this->fetchColumn();
352        }
353
354        $values = $this->_fetch();
355
356        if ($values === null) {
357            return false;
358        }
359
360        if ($values === false) {
361            throw StatementError::new($this->_stmt);
362        }
363
364        if ($fetchMode === FetchMode::NUMERIC) {
365            return $values;
366        }
367
368        assert(is_array($this->_columnNames));
369        $assoc = array_combine($this->_columnNames, $values);
370        assert(is_array($assoc));
371
372        switch ($fetchMode) {
373            case FetchMode::ASSOCIATIVE:
374                return $assoc;
375
376            case FetchMode::MIXED:
377                return $assoc + $values;
378
379            case FetchMode::STANDARD_OBJECT:
380                return (object) $assoc;
381
382            default:
383                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
384        }
385    }
386
387    /**
388     * {@inheritdoc}
389     *
390     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
391     */
392    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
393    {
394        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
395
396        $rows = [];
397
398        if ($fetchMode === FetchMode::COLUMN) {
399            while (($row = $this->fetchColumn()) !== false) {
400                $rows[] = $row;
401            }
402        } else {
403            while (($row = $this->fetch($fetchMode)) !== false) {
404                $rows[] = $row;
405            }
406        }
407
408        return $rows;
409    }
410
411    /**
412     * {@inheritdoc}
413     *
414     * @deprecated Use fetchOne() instead.
415     */
416    public function fetchColumn($columnIndex = 0)
417    {
418        $row = $this->fetch(FetchMode::NUMERIC);
419
420        if ($row === false) {
421            return false;
422        }
423
424        return $row[$columnIndex] ?? null;
425    }
426
427    /**
428     * {@inheritdoc}
429     *
430     * @deprecated The error information is available via exceptions.
431     */
432    public function fetchNumeric()
433    {
434        // do not try fetching from the statement if it's not expected to contain the result
435        // in order to prevent exceptional situation
436        if (! $this->result) {
437            return false;
438        }
439
440        $values = $this->_fetch();
441
442        if ($values === null) {
443            return false;
444        }
445
446        if ($values === false) {
447            throw StatementError::new($this->_stmt);
448        }
449
450        return $values;
451    }
452
453    /**
454     * {@inheritDoc}
455     */
456    public function fetchAssociative()
457    {
458        $values = $this->fetchNumeric();
459
460        if ($values === false) {
461            return false;
462        }
463
464        assert(is_array($this->_columnNames));
465        $row = array_combine($this->_columnNames, $values);
466        assert(is_array($row));
467
468        return $row;
469    }
470
471    /**
472     * {@inheritdoc}
473     */
474    public function fetchOne()
475    {
476        return FetchUtils::fetchOne($this);
477    }
478
479    /**
480     * {@inheritdoc}
481     */
482    public function fetchAllNumeric(): array
483    {
484        return FetchUtils::fetchAllNumeric($this);
485    }
486
487    /**
488     * {@inheritdoc}
489     */
490    public function fetchAllAssociative(): array
491    {
492        return FetchUtils::fetchAllAssociative($this);
493    }
494
495    /**
496     * {@inheritdoc}
497     */
498    public function fetchFirstColumn(): array
499    {
500        return FetchUtils::fetchFirstColumn($this);
501    }
502
503    /**
504     * {@inheritdoc}
505     */
506    public function errorCode()
507    {
508        return $this->_stmt->errno;
509    }
510
511    /**
512     * {@inheritdoc}
513     *
514     * @deprecated The error information is available via exceptions.
515     *
516     * @return string
517     */
518    public function errorInfo()
519    {
520        return $this->_stmt->error;
521    }
522
523    /**
524     * {@inheritdoc}
525     *
526     * @deprecated Use free() instead.
527     */
528    public function closeCursor()
529    {
530        $this->free();
531
532        return true;
533    }
534
535    /**
536     * {@inheritdoc}
537     */
538    public function rowCount()
539    {
540        if ($this->_columnNames === false) {
541            return $this->_stmt->affected_rows;
542        }
543
544        return $this->_stmt->num_rows;
545    }
546
547    /**
548     * {@inheritdoc}
549     */
550    public function columnCount()
551    {
552        return $this->_stmt->field_count;
553    }
554
555    public function free(): void
556    {
557        $this->_stmt->free_result();
558        $this->result = false;
559    }
560
561    /**
562     * {@inheritdoc}
563     *
564     * @deprecated Use one of the fetch- or iterate-related methods.
565     */
566    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
567    {
568        $this->_defaultFetchMode = $fetchMode;
569
570        return true;
571    }
572
573    /**
574     * {@inheritdoc}
575     *
576     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
577     */
578    #[ReturnTypeWillChange]
579    public function getIterator()
580    {
581        return new StatementIterator($this);
582    }
583}
584