Test Failed
Pull Request — master (#82)
by Wilmer
24:21 queued 13:10
created

CommandPDOSqlite::extractUsedParams()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
dl 0
loc 16
rs 9.9666
c 1
b 0
f 0
cc 4
nc 4
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\PDO;
6
7
use PDOException;
8
use Throwable;
9
use Yiisoft\Db\Cache\QueryCache;
10
use Yiisoft\Db\Command\Command;
11
use Yiisoft\Db\Connection\ConnectionPDOInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Connection\ConnectionPDOInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\Query\QueryBuilderInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\QueryBuilderInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use Yiisoft\Db\Schema\QuoterInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\QuoterInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use Yiisoft\Db\Schema\SchemaInterface;
17
use Yiisoft\Db\Sqlite\SqlToken;
18
use Yiisoft\Db\Sqlite\SqlTokenizer;
19
use Yiisoft\Strings\StringHelper;
20
21
use function array_pop;
22
use function count;
23
use function ltrim;
24
use function preg_match_all;
25
use function strpos;
26
27
final class CommandPDOSqlite extends Command
28
{
29
    public function __construct(private ConnectionPDOInterface $db, QueryCache $queryCache)
30
    {
31
        parent::__construct($queryCache);
0 ignored issues
show
Bug introduced by
The call to Yiisoft\Db\Command\Command::__construct() has too few arguments starting with queryCache. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

31
        parent::/** @scrutinizer ignore-call */ 
32
                __construct($queryCache);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
$queryCache of type Yiisoft\Db\Cache\QueryCache is incompatible with the type Yiisoft\Db\Connection\ConnectionInterface expected by parameter $db of Yiisoft\Db\Command\Command::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

31
        parent::__construct(/** @scrutinizer ignore-type */ $queryCache);
Loading history...
32
    }
33
34
    public function queryBuilder(): QueryBuilderInterface
35
    {
36
        return $this->db->getQueryBuilder();
37
    }
38
39
    public function prepare(?bool $forRead = null): void
40
    {
41
        if (isset($this->pdoStatement)) {
0 ignored issues
show
Bug introduced by
The property pdoStatement is declared private in Yiisoft\Db\Command\Command and cannot be accessed from this context.
Loading history...
42
            $this->bindPendingParams();
43
44
            return;
45
        }
46
47
        $sql = $this->getSql() ?? '';
48
49
        if ($this->db->getTransaction()) {
50
            /** master is in a transaction. use the same connection. */
51
            $forRead = false;
52
        }
53
54
        if ($forRead || ($forRead === null && $this->db->getSchema()->isReadQuery($sql))) {
55
            $pdo = $this->db->getSlavePdo();
56
        } else {
57
            $pdo = $this->db->getMasterPdo();
58
        }
59
60
        try {
61
            $this->pdoStatement = $pdo?->prepare($sql);
62
            $this->bindPendingParams();
63
        } catch (PDOException $e) {
64
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
65
            $errorInfo = $e->errorInfo ?? null;
66
67
            throw new Exception($message, $errorInfo, $e);
68
        }
69
    }
70
71
    /**
72
     * Executes the SQL statement.
73
     *
74
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
75
     * No result set will be returned.
76
     *
77
     * @throws Exception|Throwable execution failed.
78
     *
79
     * @return int number of rows affected by the execution.
80
     */
81
    public function execute(): int
82
    {
83
        $sql = $this->getSql() ?? '';
84
85
        /** @var array<string, string> */
86
        $params = $this->params;
87
88
        $statements = $this->splitStatements($sql, $params);
89
90
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
91
            return parent::execute();
92
        }
93
94
        $result = 0;
95
96
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
97
        foreach ($statements as $statement) {
98
            [$statementSql, $statementParams] = $statement;
99
            $statementSql = is_string($statementSql) ? $statementSql : '';
100
            $statementParams = is_array($statementParams) ? $statementParams : [];
101
            $this->setSql($statementSql)->bindValues($statementParams);
102
            $result = parent::execute();
103
        }
104
105
        $this->setSql($sql)->bindValues($params);
106
107
        return $result;
108
    }
109
110
    protected function getCacheKey(string $method, ?int $fetchMode, string $rawSql): array
111
    {
112
        return [
113
            __CLASS__,
114
            $method,
115
            $fetchMode,
116
            $this->db->getDriver()->getDsn(),
117
            $this->db->getDriver()->getUsername(),
118
            $rawSql,
119
        ];
120
    }
121
122
    protected function internalExecute(?string $rawSql): void
123
    {
124
        $attempt = 0;
125
126
        while (true) {
127
            try {
128
                if (
129
                    ++$attempt === 1
130
                    && $this->isolationLevel !== null
0 ignored issues
show
Bug introduced by
The property isolationLevel is declared private in Yiisoft\Db\Command\Command and cannot be accessed from this context.
Loading history...
131
                    && $this->db->getTransaction() === null
132
                ) {
133
                    $this->db->transaction(fn (?string $rawSql) => $this->internalExecute($rawSql), $this->isolationLevel);
134
                } else {
135
                    $this->pdoStatement?->execute();
0 ignored issues
show
Bug introduced by
The property pdoStatement is declared private in Yiisoft\Db\Command\Command and cannot be accessed from this context.
Loading history...
136
                }
137
                break;
138
            } catch (PDOException $e) {
139
                $rawSql = $rawSql ?: $this->getRawSql();
140
                $e = $this->db->getSchema()->convertException($e, $rawSql);
141
142
                if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) {
0 ignored issues
show
Bug introduced by
The property retryHandler is declared private in Yiisoft\Db\Command\Command and cannot be accessed from this context.
Loading history...
143
                    throw $e;
144
                }
145
            }
146
        }
147
    }
148
149
    /**
150
     * Performs the actual DB query of a SQL statement.
151
     *
152
     * @param string $method method of PDOStatement to be called
153
     * @param array|int|null $fetchMode the result fetch mode.
154
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
155
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
156
     *
157
     * @throws Exception|Throwable if the query causes any problem.
158
     *
159
     * @return mixed the method execution result.
160
     */
161
    protected function queryInternal(string $method, array|int $fetchMode = null): mixed
162
    {
163
        $sql = $this->getSql() ?? '';
164
165
        /** @var array<string, string> */
166
        $params = $this->params;
167
168
        $statements = $this->splitStatements($sql, $params);
169
170
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
171
            return parent::queryInternal($method, $fetchMode);
172
        }
173
174
        [$lastStatementSql, $lastStatementParams] = array_pop($statements);
175
176
        /**
177
         * @psalm-var array<array-key, array> $statements
178
         */
179
        foreach ($statements as $statement) {
180
            /**
181
             * @var string $statementSql
182
             * @var array $statementParams
183
             */
184
            [$statementSql, $statementParams] = $statement;
185
            $this->setSql($statementSql)->bindValues($statementParams);
186
            parent::execute();
187
        }
188
189
        $this->setSql($lastStatementSql)->bindValues($lastStatementParams);
190
191
        /** @var string */
192
        $result = parent::queryInternal($method, $fetchMode);
193
194
        $this->setSql($sql)->bindValues($params);
195
196
        return $result;
197
    }
198
199
    /**
200
     * Splits the specified SQL codes into individual SQL statements and returns them or `false` if there's a single
201
     * statement.
202
     *
203
     * @param string $sql
204
     * @param array $params
205
     *
206
     * @throws InvalidArgumentException
207
     *
208
     * @return array|bool (array|string)[][]|bool
209
     *
210
     * @psalm-param array<string, string> $params
211
     * @psalm-return false|list<array{0: string, 1: array}>
212
     */
213
    private function splitStatements(string $sql, array $params): bool|array
214
    {
215
        $semicolonIndex = strpos($sql, ';');
216
217
        if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
218
            return false;
219
        }
220
221
        $tokenizer = new SqlTokenizer($sql);
222
223
        $codeToken = $tokenizer->tokenize();
224
225
        if (count($codeToken->getChildren()) === 1) {
226
            return false;
227
        }
228
229
        $statements = [];
230
231
        foreach ($codeToken->getChildren() as $statement) {
232
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
233
        }
234
235
        return $statements;
236
    }
237
238
    /**
239
     * Returns named bindings used in the specified statement token.
240
     *
241
     * @param SqlToken $statement
242
     * @param array $params
243
     *
244
     * @return array
245
     *
246
     * @psalm-param array<string, string> $params
247
     */
248
    private function extractUsedParams(SqlToken $statement, array $params): array
249
    {
250
        preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
251
252
        $result = [];
253
254
        foreach ($matches as $match) {
255
            $phName = ltrim($match['placeholder'], ':');
256
            if (isset($params[$phName])) {
257
                $result[$phName] = $params[$phName];
258
            } elseif (isset($params[':' . $phName])) {
259
                $result[':' . $phName] = $params[':' . $phName];
260
            }
261
        }
262
263
        return $result;
264
    }
265
}
266