Passed
Pull Request — master (#656)
by Wilmer
11:05 queued 08:56
created

AbstractCommand::bindParam()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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