Passed
Pull Request — master (#684)
by Def
06:06 queued 03:45
created

AbstractPdoCommand::internalGetQueryResult()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 6
nop 1
dl 0
loc 26
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Driver\Pdo;
6
7
use PDO;
8
use PDOException;
9
use PDOStatement;
10
use Psr\Log\LoggerAwareInterface;
11
use Psr\Log\LoggerAwareTrait;
12
use Psr\Log\LogLevel;
13
use Throwable;
14
use Yiisoft\Db\Command\AbstractCommand;
15
use Yiisoft\Db\Command\Param;
16
use Yiisoft\Db\Command\ParamInterface;
17
use Yiisoft\Db\Exception\Exception;
18
use Yiisoft\Db\Exception\InvalidParamException;
19
use Yiisoft\Db\Profiler\Context\CommandContext;
20
use Yiisoft\Db\Profiler\ProfilerAwareInterface;
21
use Yiisoft\Db\Profiler\ProfilerAwareTrait;
22
use Yiisoft\Db\Query\Data\DataReader;
23
24
/**
25
 * Represents a database command that can be executed using a PDO (PHP Data Object) database connection.
26
 *
27
 * It's an abstract class that provides a common interface for building and executing various types of statements
28
 * such as {@see cancel()}, {@see execute()}, {@see insert()}, {@see update()}, {@see delete()}, etc., using a PDO
29
 * connection.
30
 *
31
 * It also provides methods for binding parameter values and retrieving query results.
32
 */
33
abstract class AbstractPdoCommand extends AbstractCommand implements PdoCommandInterface, LoggerAwareInterface, ProfilerAwareInterface
34
{
35
    use LoggerAwareTrait;
36
    use ProfilerAwareTrait;
37
38
    /**
39
     * @var PDOStatement|null Represents a prepared statement and, after the statement is executed, an associated
40
     * result set.
41
     *
42
     * @link https://www.php.net/manual/en/class.pdostatement.php
43
     */
44
    protected PDOStatement|null $pdoStatement = null;
45
46
    public function __construct(protected PdoConnectionInterface $db)
47
    {
48
    }
49
50
    /**
51
     * This method mainly sets {@see pdoStatement} to be `null`.
52
     */
53
    public function cancel(): void
54
    {
55
        $this->pdoStatement = null;
56
    }
57
58
    public function getPdoStatement(): PDOStatement|null
59
    {
60
        return $this->pdoStatement;
61
    }
62
63
    public function bindParam(
64
        int|string $name,
65
        mixed &$value,
66
        int|null $dataType = null,
67
        int|null $length = null,
68
        mixed $driverOptions = null
69
    ): static {
70
        $this->prepare();
71
72
        if ($dataType === null) {
73
            $dataType = $this->db->getSchema()->getPdoType($value);
74
        }
75
76
        if ($length === null) {
77
            $this->pdoStatement?->bindParam($name, $value, $dataType);
78
        } elseif ($driverOptions === null) {
79
            $this->pdoStatement?->bindParam($name, $value, $dataType, $length);
80
        } else {
81
            $this->pdoStatement?->bindParam($name, $value, $dataType, $length, $driverOptions);
82
        }
83
84
        return $this;
85
    }
86
87
    public function bindValue(int|string $name, mixed $value, int|null $dataType = null): static
88
    {
89
        if ($dataType === null) {
90
            $dataType = $this->db->getSchema()->getPdoType($value);
91
        }
92
93
        $this->params[$name] = new Param($value, $dataType);
94
95
        return $this;
96
    }
97
98
    public function bindValues(array $values): static
99
    {
100
        if (empty($values)) {
101
            return $this;
102
        }
103
104
        /**
105
         * @psalm-var array<string, int>|ParamInterface|int $value
106
         */
107
        foreach ($values as $name => $value) {
108
            if ($value instanceof ParamInterface) {
109
                $this->params[$name] = $value;
110
            } else {
111
                $type = $this->db->getSchema()->getPdoType($value);
112
                $this->params[$name] = new Param($value, $type);
113
            }
114
        }
115
116
        return $this;
117
    }
118
119
    public function prepare(bool|null $forRead = null): void
120
    {
121
        if (isset($this->pdoStatement)) {
122
            $this->bindPendingParams();
123
124
            return;
125
        }
126
127
        $sql = $this->getSql();
128
129
        /**
130
         * If SQL is empty, there will be {@see \ValueError} on prepare pdoStatement.
131
         *
132
         * @link https://php.watch/versions/8.0/ValueError
133
         */
134
        if ($sql === '') {
135
            return;
136
        }
137
138
        $pdo = $this->db->getActivePDO($sql, $forRead);
139
140
        try {
141
            $this->pdoStatement = $pdo?->prepare($sql);
142
            $this->bindPendingParams();
143
        } catch (PDOException $e) {
144
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
145
            /** @psalm-var array|null $errorInfo */
146
            $errorInfo = $e->errorInfo ?? null;
147
148
            throw new Exception($message, $errorInfo, $e);
149
        }
150
    }
151
152
    /**
153
     * Binds pending parameters registered via {@see bindValue()} and {@see bindValues()}.
154
     *
155
     * Note that this method requires an active {@see pdoStatement}.
156
     */
157
    protected function bindPendingParams(): void
158
    {
159
        foreach ($this->params as $name => $value) {
160
            $this->pdoStatement?->bindValue($name, $value->getValue(), $value->getType());
161
        }
162
    }
163
164
    protected function getQueryMode(int $queryMode): string
165
    {
166
        return match ($queryMode) {
167
            self::QUERY_MODE_EXECUTE => 'execute',
168
            self::QUERY_MODE_ROW => 'queryOne',
169
            self::QUERY_MODE_ALL => 'queryAll',
170
            self::QUERY_MODE_COLUMN => 'queryColumn',
171
            self::QUERY_MODE_CURSOR => 'query',
172
            self::QUERY_MODE_SCALAR => 'queryScalar',
173
            self::QUERY_MODE_ROW | self::QUERY_MODE_EXECUTE => 'insertWithReturningPks'
174
        };
175
    }
176
177
    /**
178
     * Executes a prepared statement.
179
     *
180
     * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers.
181
     *
182
     * @param string|null $rawSql The rawSql if it has been created.
183
     *
184
     * @throws Exception
185
     * @throws Throwable
186
     */
187
    abstract protected function internalExecute(string|null $rawSql): void;
188
189
    /**
190
     * @throws InvalidParamException
191
     */
192
    protected function internalGetQueryResult(int $queryMode): mixed
193
    {
194
        if ($queryMode === self::QUERY_MODE_CURSOR) {
195
            return new DataReader($this);
196
        }
197
198
        if ($queryMode === self::QUERY_MODE_EXECUTE) {
199
            return $this->pdoStatement?->rowCount() ?? 0;
200
        }
201
202
        if ($this->is($queryMode, self::QUERY_MODE_ROW) || $this->is($queryMode, self::QUERY_MODE_SCALAR)) {
203
            /** @psalm-var array|false $result */
204
            $result = $this->pdoStatement?->fetch(PDO::FETCH_ASSOC);
205
        } elseif ($this->is($queryMode, self::QUERY_MODE_COLUMN)) {
206
            /** @psalm-var mixed $result */
207
            $result = $this->pdoStatement?->fetchAll(PDO::FETCH_COLUMN);
208
        } elseif ($this->is($queryMode, self::QUERY_MODE_ALL)) {
209
            /** @psalm-var mixed $result */
210
            $result = $this->pdoStatement?->fetchAll(PDO::FETCH_ASSOC);
211
        } else {
212
            throw new InvalidParamException("Unknown query mode '$queryMode'");
213
        }
214
215
        $this->pdoStatement?->closeCursor();
216
217
        return $result;
218
    }
219
220
    /**
221
     * Logs the current database query if query logging is on and returns the profiling token if profiling is on.
222
     */
223
    protected function logQuery(string $rawSql, string $category): void
224
    {
225
        $this->logger?->log(LogLevel::INFO, $rawSql, [$category]);
226
    }
227
228
    protected function queryInternal(int $queryMode): mixed
229
    {
230
        $rawSql = $this->getRawSql();
231
        $logCategory = self::class . '::' . $this->getQueryMode($queryMode);
232
233
        $this->logQuery($rawSql, $logCategory);
234
235
        $queryContext = new CommandContext(__METHOD__, $logCategory, $this->getSql(), $this->getParams());
236
237
        $this->profiler?->begin($rawSql, $queryContext);
238
        try {
239
            /** @psalm-var mixed $result */
240
            $result = parent::queryInternal($queryMode);
241
        } catch (Exception $e) {
242
            $this->profiler?->end($rawSql, $queryContext->setException($e));
243
            throw $e;
244
        }
245
        $this->profiler?->end($rawSql, $queryContext);
246
247
        return $result;
248
    }
249
250
    /**
251
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
252
     */
253
    protected function refreshTableSchema(): void
254
    {
255
        if ($this->refreshTableName !== null) {
256
            $this->db->getSchema()->refreshTableSchema($this->refreshTableName);
257
        }
258
    }
259
}
260