packages/ztd-query-mysqli-adapter/src/Driver/MysqliStatement.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Adapter\Mysqli\Driver;
6
7use mysqli;
8use mysqli_result;
9use mysqli_stmt;
10use ZtdQuery\Connection\Exception\DatabaseException;
11use ZtdQuery\Connection\StatementInterface;
12use ZtdQuery\Platform\ResultColumnTypeResolver;
13
14/**
15 * mysqli prepared statement adapter implementing StatementInterface for ZTD layer.
16 *
17 * This class wraps a mysqli_stmt and provides the minimal interface
18 * required by the ZTD session for executing statements and fetching results.
19 */
20final class MysqliStatement implements StatementInterface
21{
22    private mysqli_stmt $statement;
23
24    private mysqli $mysqli;
25
26    /**
27     * @var mysqli_result|false|null
28     */
29    private mysqli_result|false|null $result = null;
30
31    /**
32     * Adapt a prepared native statement and retain its connection for error details.
33     */
34    public function __construct(mysqli_stmt $statement, mysqli $mysqli)
35    {
36        $this->statement = $statement;
37        $this->mysqli = $mysqli;
38    }
39
40    /**
41     * {@inheritDoc}
42     *
43     * @throws DatabaseException On database error.
44     */
45    public function execute(?array $params = null): bool
46    {
47        if ($params !== null && $params !== []) {
48            if (!$this->statement->execute($params)) {
49                if ($this->mysqli->errno !== 0) {
50                    throw new DatabaseException(
51                        $this->mysqli->error,
52                        $this->mysqli->errno,
53                        $this->mysqli->errno
54                    );
55                }
56                return false;
57            }
58        } else {
59            if (!$this->statement->execute()) {
60                if ($this->mysqli->errno !== 0) {
61                    throw new DatabaseException(
62                        $this->mysqli->error,
63                        $this->mysqli->errno,
64                        $this->mysqli->errno
65                    );
66                }
67                return false;
68            }
69        }
70
71        /**
72         * get_result() is deferred so ZtdMysqliStatement::get_result() can call it on the underlying stmt
73         */
74
75        return true;
76    }
77
78    /**
79     * {@inheritDoc}
80     */
81    public function fetchAll(): array
82    {
83        $result = $this->result ??= $this->statement->get_result();
84
85        if ($result === false) {
86            $this->statement->close();
87            return [];
88        }
89
90        $rows = mysqli_fetch_all($result, MYSQLI_ASSOC);
91
92        /**
93         * Free the result to avoid "Commands out of sync" errors
94         */
95        $result->free();
96        $this->result = null;
97
98        $this->statement->close();
99
100        return $rows;
101    }
102
103    /**
104     * {@inheritDoc}
105     */
106    public function resultColumns(ResultColumnTypeResolver $typeResolver): array
107    {
108        $result = $this->result ??= $this->statement->get_result();
109        if ($result === false) {
110            return [];
111        }
112
113        return MysqliResultColumnExtractor::extract($result, $typeResolver);
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    public function rowCount(): int
120    {
121        return (int) $this->statement->affected_rows;
122    }
123
124}
125