Passed
Push — master ( e3e48a...a5cdfd )
by Def
02:27
created

AbstractCommandPDO::insertWithReturningPks()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 2
dl 0
loc 25
rs 9.7998
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\Cache\QueryCache;
12
use Yiisoft\Db\Command\AbstractCommand;
13
use Yiisoft\Db\Command\Param;
14
use Yiisoft\Db\Command\ParamInterface;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Exception\InvalidParamException;
18
use Yiisoft\Db\Query\Data\DataReader;
19
20
abstract class AbstractCommandPDO extends AbstractCommand implements CommandPDOInterface
21
{
22
    protected PDOStatement|null $pdoStatement = null;
23
24
    public function __construct(protected ConnectionPDOInterface $db, QueryCache $queryCache)
25
    {
26
        parent::__construct($queryCache);
27
    }
28
29
    /**
30
     * @inheritDoc
31
     * This method mainly sets {@see pdoStatement} to be null.
32
     */
33
    public function cancel(): void
34
    {
35
        $this->pdoStatement = null;
36
    }
37
38
    public function getPdoStatement(): PDOStatement|null
39
    {
40
        return $this->pdoStatement;
41
    }
42
43
    /**
44
     * @inheritDoc
45
     *
46
     * @link http://www.php.net/manual/en/function.PDOStatement-bindParam.php
47
     */
48
    public function bindParam(
49
        int|string $name,
50
        mixed &$value,
51
        int|null $dataType = null,
52
        int|null $length = null,
53
        mixed $driverOptions = null
54
    ): static {
55
        $this->prepare();
56
57
        if ($dataType === null) {
58
            $dataType = $this->db->getSchema()->getPdoType($value);
59
        }
60
61
        if ($length === null) {
62
            $this->pdoStatement?->bindParam($name, $value, $dataType);
63
        } elseif ($driverOptions === null) {
64
            $this->pdoStatement?->bindParam($name, $value, $dataType, $length);
65
        } else {
66
            $this->pdoStatement?->bindParam($name, $value, $dataType, $length, $driverOptions);
67
        }
68
69
        return $this;
70
    }
71
72
    public function bindValue(int|string $name, mixed $value, int|null $dataType = null): static
73
    {
74
        if ($dataType === null) {
75
            $dataType = $this->db->getSchema()->getPdoType($value);
76
        }
77
78
        $this->params[$name] = new Param($value, $dataType);
79
80
        return $this;
81
    }
82
83
    public function bindValues(array $values): static
84
    {
85
        if (empty($values)) {
86
            return $this;
87
        }
88
89
        /**
90
         * @psalm-var array<string, int>|ParamInterface|int $value
91
         */
92
        foreach ($values as $name => $value) {
93
            if ($value instanceof ParamInterface) {
94
                $this->params[$name] = $value;
95
            } else {
96
                $type = $this->db->getSchema()->getPdoType($value);
97
                $this->params[$name] = new Param($value, $type);
98
            }
99
        }
100
101
        return $this;
102
    }
103
104
    public function insertWithReturningPks(string $table, array $columns): bool|array
105
    {
106
        $params = [];
107
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
108
        $this->setSql($sql)->bindValues($params);
109
110
        if (!$this->execute()) {
111
            return false;
112
        }
113
114
        $tableSchema = $this->db->getSchema()->getTableSchema($table);
115
        $tablePrimaryKeys = $tableSchema?->getPrimaryKey() ?? [];
116
117
        $result = [];
118
        foreach ($tablePrimaryKeys as $name) {
119
            if ($tableSchema?->getColumn($name)?->isAutoIncrement()) {
120
                $result[$name] = $this->db->getLastInsertID((string) $tableSchema?->getSequenceName());
121
                continue;
122
            }
123
124
            /** @psalm-var mixed */
125
            $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue();
126
        }
127
128
        return $result;
129
    }
130
131
    /**
132
     * @throws Exception|InvalidConfigException|PDOException
133
     */
134
    public function prepare(bool|null $forRead = null): void
135
    {
136
        if (isset($this->pdoStatement)) {
137
            $this->bindPendingParams();
138
139
            return;
140
        }
141
142
        $sql = $this->getSql();
143
144
        $pdo = $this->db->getActivePDO($sql, $forRead);
145
146
        try {
147
            $this->pdoStatement = $pdo?->prepare($sql);
148
            $this->bindPendingParams();
149
        } catch (PDOException $e) {
150
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
151
            /** @var array|null */
152
            $errorInfo = $e->errorInfo ?? null;
153
154
            throw new Exception($message, $errorInfo, $e);
155
        }
156
    }
157
158
    /**
159
     * Binds pending parameters that were registered via {@see bindValue()} and {@see bindValues()}.
160
     *
161
     * Note that this method requires an active {@see pdoStatement}.
162
     */
163
    protected function bindPendingParams(): void
164
    {
165
        foreach ($this->params as $name => $value) {
166
            $this->pdoStatement?->bindValue($name, $value->getValue(), $value->getType());
167
        }
168
    }
169
170
    protected function getCacheKey(int $queryMode, string $rawSql): array
171
    {
172
        return array_merge([static::class , $queryMode], $this->db->getCacheKey(), [$rawSql]);
173
    }
174
175
    /**
176
     * @throws InvalidParamException
177
     */
178
    protected function internalGetQueryResult(int $queryMode): mixed
179
    {
180
        if ($queryMode === static::QUERY_MODE_CURSOR) {
181
            return new DataReader($this);
182
        }
183
184
        if ($queryMode === static::QUERY_MODE_NONE) {
185
            return $this->pdoStatement?->rowCount() ?? 0;
186
        }
187
188
        if ($queryMode === static::QUERY_MODE_ROW) {
189
            /** @var mixed */
190
            $result = $this->pdoStatement?->fetch(PDO::FETCH_ASSOC);
191
        } elseif ($queryMode === static::QUERY_MODE_COLUMN) {
192
            /** @var mixed */
193
            $result = $this->pdoStatement?->fetchAll(PDO::FETCH_COLUMN);
194
        } else {
195
            /** @var mixed */
196
            $result = $this->pdoStatement?->fetchAll(PDO::FETCH_ASSOC);
197
        }
198
199
        $this->pdoStatement?->closeCursor();
200
201
        return $result;
202
    }
203
204
    /**
205
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
206
     */
207
    protected function refreshTableSchema(): void
208
    {
209
        if ($this->refreshTableName !== null) {
210
            $this->db->getSchema()->refreshTableSchema($this->refreshTableName);
211
        }
212
    }
213
214
    /**
215
     * Executes a prepared statement.
216
     *
217
     * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers.
218
     *
219
     * @param string|null $rawSql the rawSql if it has been created.
220
     *
221
     * @throws Exception|Throwable
222
     */
223
    abstract protected function internalExecute(string|null $rawSql): void;
224
}
225