Passed
Pull Request — master (#793)
by Alexander
10:27 queued 08:06
created

AbstractPdoCommand::logQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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