Passed
Pull Request — master (#671)
by Wilmer
14:35 queued 12:26
created

AbstractPdoCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 2
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\Exception;
15
use Yiisoft\Db\Exception\InvalidParamException;
16
use Yiisoft\Db\Query\Data\DataReader;
17
18
/**
19
 * Represents a database command that can be executed using a PDO (PHP Data Object) database connection.
20
 *
21
 * It's an abstract class that provides a common interface for building and executing various types of statements
22
 * such as {@see cancel()}, {@see execute()}, {@see insert()}, {@see update()}, {@see delete()}, etc., using a PDO
23
 * connection.
24
 *
25
 * It also provides methods for binding parameter values and retrieving query results.
26
 */
27
abstract class AbstractPdoCommand extends AbstractCommand implements PdoCommandInterface
28
{
29
    /**
30
     * @var PDOStatement|null Represents a prepared statement and, after the statement is executed, an associated
31
     * result set.
32
     *
33
     * @link https://www.php.net/manual/en/class.pdostatement.php
34
     */
35
    protected PDOStatement|null $pdoStatement = null;
36
37
    public function __construct(protected PdoConnectionInterface $db)
38
    {
39
    }
40
41
    /**
42
     * This method mainly sets {@see pdoStatement} to be `null`.
43
     */
44
    public function cancel(): void
45
    {
46
        $this->pdoStatement = null;
47
    }
48
49
    public function getPdoStatement(): PDOStatement|null
50
    {
51
        return $this->pdoStatement;
52
    }
53
54
    public function bindParam(
55
        int|string $name,
56
        mixed &$value,
57
        int|null $dataType = null,
58
        int|null $length = null,
59
        mixed $driverOptions = null
60
    ): static {
61
        $this->prepare();
62
63
        if ($dataType === null) {
64
            $dataType = $this->db->getSchema()->getPdoType($value);
65
        }
66
67
        if ($length === null) {
68
            $this->pdoStatement?->bindParam($name, $value, $dataType);
69
        } elseif ($driverOptions === null) {
70
            $this->pdoStatement?->bindParam($name, $value, $dataType, $length);
71
        } else {
72
            $this->pdoStatement?->bindParam($name, $value, $dataType, $length, $driverOptions);
73
        }
74
75
        return $this;
76
    }
77
78
    public function bindValue(int|string $name, mixed $value, int|null $dataType = null): static
79
    {
80
        if ($dataType === null) {
81
            $dataType = $this->db->getSchema()->getPdoType($value);
82
        }
83
84
        $this->params[$name] = new Param($value, $dataType);
85
86
        return $this;
87
    }
88
89
    public function bindValues(array $values): static
90
    {
91
        if (empty($values)) {
92
            return $this;
93
        }
94
95
        /**
96
         * @psalm-var array<string, int>|ParamInterface|int $value
97
         */
98
        foreach ($values as $name => $value) {
99
            if ($value instanceof ParamInterface) {
100
                $this->params[$name] = $value;
101
            } else {
102
                $type = $this->db->getSchema()->getPdoType($value);
103
                $this->params[$name] = new Param($value, $type);
104
            }
105
        }
106
107
        return $this;
108
    }
109
110
    public function prepare(bool|null $forRead = null): void
111
    {
112
        if (isset($this->pdoStatement)) {
113
            $this->bindPendingParams();
114
115
            return;
116
        }
117
118
        $sql = $this->getSql();
119
120
        /**
121
         * If SQL is empty, there will be {@see \ValueError} on prepare pdoStatement.
122
         *
123
         * @link https://php.watch/versions/8.0/ValueError
124
         */
125
        if ($sql === '') {
126
            return;
127
        }
128
129
        $pdo = $this->db->getActivePDO($sql, $forRead);
130
131
        try {
132
            $this->pdoStatement = $pdo?->prepare($sql);
133
            $this->bindPendingParams();
134
        } catch (PDOException $e) {
135
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
136
            /** @psalm-var array|null $errorInfo */
137
            $errorInfo = $e->errorInfo ?? null;
138
139
            throw new Exception($message, $errorInfo, $e);
140
        }
141
    }
142
143
    /**
144
     * Binds pending parameters registered via {@see bindValue()} and {@see bindValues()}.
145
     *
146
     * Note that this method requires an active {@see pdoStatement}.
147
     */
148
    protected function bindPendingParams(): void
149
    {
150
        foreach ($this->params as $name => $value) {
151
            $this->pdoStatement?->bindValue($name, $value->getValue(), $value->getType());
152
        }
153
    }
154
155
    /**
156
     * Executes a prepared statement.
157
     *
158
     * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers.
159
     *
160
     * @param string|null $rawSql The rawSql if it has been created.
161
     *
162
     * @throws Exception
163
     * @throws Throwable
164
     */
165
    abstract protected function internalExecute(string|null $rawSql): void;
166
167
    /**
168
     * @throws InvalidParamException
169
     */
170
    protected function internalGetQueryResult(int $queryMode): mixed
171
    {
172
        if ($queryMode === self::QUERY_MODE_CURSOR) {
173
            return new DataReader($this);
174
        }
175
176
        if ($queryMode === self::QUERY_MODE_EXECUTE) {
177
            return $this->pdoStatement?->rowCount() ?? 0;
178
        }
179
180
        if ($this->is($queryMode, self::QUERY_MODE_ROW)) {
181
            /** @psalm-var array|false $result */
182
            $result = $this->pdoStatement?->fetch(PDO::FETCH_ASSOC);
183
        } elseif ($this->is($queryMode, self::QUERY_MODE_COLUMN)) {
184
            /** @psalm-var mixed $result */
185
            $result = $this->pdoStatement?->fetchAll(PDO::FETCH_COLUMN);
186
        } elseif ($this->is($queryMode, self::QUERY_MODE_ALL)) {
187
            /** @psalm-var mixed $result */
188
            $result = $this->pdoStatement?->fetchAll(PDO::FETCH_ASSOC);
189
        } else {
190
            throw new InvalidParamException("Unknown query mode '$queryMode'");
191
        }
192
193
        $this->pdoStatement?->closeCursor();
194
195
        return $result;
196
    }
197
198
    /**
199
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
200
     */
201
    protected function refreshTableSchema(): void
202
    {
203
        if ($this->refreshTableName !== null) {
204
            $this->db->getSchema()->refreshTableSchema($this->refreshTableName);
205
        }
206
    }
207
}
208