Passed
Pull Request — master (#806)
by Sergei
02:26
created

ExpressionBuilder::appendParams()   B

Complexity

Conditions 8
Paths 25

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 17
c 1
b 0
f 0
nc 25
nop 3
dl 0
loc 33
ccs 0
cts 0
cp 0
crap 72
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Expression;
6
7
use Yiisoft\Db\Connection\ConnectionInterface;
8
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
9
use Yiisoft\Db\Syntax\SqlParser;
10
11
use function array_merge;
12
use function strlen;
13
use function substr;
14
use function substr_replace;
15
16
/**
17
 * It's used to build expressions for use in database queries.
18
 *
19 333
 * It provides a methods {@see build()} for creating various types of expressions, such as conditions, joins, and
20
 * ordering clauses.
21 333
 *
22
 * These expressions can be used with the query builder to build complex and customizable database queries
23 333
 * {@see Expression} class.
24
 *
25
 * @psalm-import-type ParamsType from ConnectionInterface
26
 */
27
class ExpressionBuilder implements ExpressionBuilderInterface
28
{
29
    public function __construct(private QueryBuilderInterface|null $queryBuilder = null)
30
    {
31
    }
32
33
    /**
34
     * Builds SQL statement from the given expression.
35
     *
36
     * @param Expression $expression The expression to be built.
37
     * @param array $params The parameters to be bound to the query.
38
     *
39
     * @psalm-param ParamsType $params
40
     *
41
     * @return string SQL statement.
42
     */
43
    public function build(Expression $expression, array &$params = []): string
44
    {
45
        $sql = $expression->__toString();
46
        $expressionParams = $expression->getParams();
47
48
        if (empty($expressionParams)) {
49
            return $sql;
50
        }
51
52
        if ($this->queryBuilder === null || isset($expressionParams[0])) {
53
            $params = array_merge($params, $expressionParams);
54
            return $sql;
55
        }
56
57
        $sql = $this->appendParams($sql, $expressionParams, $params);
58
59
        return $this->replaceParamExpressions($sql, $expressionParams, $params);
60
    }
61
62
    /**
63
     * Appends parameters to the list of query parameters replacing non-unique parameters with unique ones.
64
     *
65
     * @param string $sql SQL statement of the expression.
66
     * @param array $expressionParams Parameters to be appended.
67
     * @param array $params Parameters to be bound to the query.
68
     *
69
     * @psalm-param ParamsType $params
70
     *
71
     * @return string SQL statement with unique parameters.
72
     */
73
    private function appendParams(string $sql, array &$expressionParams, array &$params): string
74
    {
75
        $nonUniqueParams = [];
76
77
        /** @var non-empty-string $name */
78
        foreach ($expressionParams as $name => $value) {
79
            $paramName = $name[0] === ':' ? substr($name, 1) : $name;
80
81
            if (!isset($params[$paramName]) && !isset($params[":$paramName"])) {
82
                $params[$name] = $value;
83
                continue;
84
            }
85
86
            $nonUniqueParams[$name] = $value;
87
        }
88
89
        /** @var non-empty-string $name */
90
        foreach ($nonUniqueParams as $name => $value) {
91
            $paramName = $name[0] === ':' ? substr($name, 1) : $name;
92
            $uniqueName = $this->getUniqueName($paramName, $params);
93
94
            $sql = $this->replacePlaceholder($sql, ":$paramName", ":$uniqueName");
95
96
            if ($name[0] === ':') {
97
                $uniqueName = ":$uniqueName";
98
            }
99
100
            $params[$uniqueName] = $value;
101
            $expressionParams[$uniqueName] = $value;
102
            unset($expressionParams[$name]);
103
        }
104
105
        return $sql;
106
    }
107
108
    /**
109
     * Replaces parameters with expression values in SQL statement.
110
     *
111
     * @param string $sql SQL statement where parameters should be replaced.
112
     * @param array $expressionParams Parameters to be replaced.
113
     * @param array $params Parameters to be bound to the query.
114
     *
115
     * @psalm-param ParamsType $expressionParams
116
     * @psalm-param ParamsType $params
117
     *
118
     * @return string SQL statement with replaced parameters.
119
     */
120
    private function replaceParamExpressions(string $sql, array $expressionParams, array &$params): string
121
    {
122
        /** @var non-empty-string $name */
123
        foreach ($expressionParams as $name => $value) {
124
            if (!$value instanceof ExpressionInterface) {
125
                continue;
126
            }
127
128
            $placeholder = $name[0] !== ':' ? ":$name" : $name;
129
            /** @psalm-suppress PossiblyNullReference */
130
            $replacement = $this->queryBuilder->buildExpression($value, $params);
0 ignored issues
show
Bug introduced by
The method buildExpression() does not exist on null. ( Ignorable by Annotation )

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

130
            /** @scrutinizer ignore-call */ 
131
            $replacement = $this->queryBuilder->buildExpression($value, $params);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
131
132
            $sql = $this->replacePlaceholder($sql, $placeholder, $replacement);
133
134
            /** @psalm-var ParamsType $params */
135
            unset($params[$name]);
136
        }
137
138
        return $sql;
139
    }
140
141
    /**
142
     * Returns a unique name for the parameter without colon at the beginning.
143
     *
144
     * @param string $name Name of the parameter without colon at the beginning.
145
     * @param array $params Parameters to be bound to the query.
146
     *
147
     * @psalm-param ParamsType $params
148
     *
149
     * @return string Unique name of the parameter with colon at the beginning.
150
     *
151
     * @psalm-return non-empty-string
152
     */
153
    private function getUniqueName(string $name, array $params): string
154
    {
155
        $uniqueName = $name . '_0';
156
157
        for ($i = 1; isset($params[$uniqueName]) || isset($params[":$uniqueName"]); ++$i) {
158
            $uniqueName = $name . '_' . $i;
159
        }
160
161
        return $uniqueName;
162
    }
163
164
    /**
165
     * Replaces the placeholder with the replacement in SQL statement.
166
     *
167
     * @param string $sql SQL statement where the placeholder should be replaced.
168
     * @param string $placeholder Placeholder to be replaced.
169
     * @param string $replacement Replacement for the placeholder.
170
     *
171
     * @return string SQL with the replaced placeholder.
172
     */
173
    private function replacePlaceholder(string $sql, string $placeholder, string $replacement): string
174
    {
175
        $parser = $this->createSqlParser($sql);
176
177
        while (null !== $parsedPlaceholder = $parser->getNextPlaceholder($position)) {
178
            if ($parsedPlaceholder === $placeholder) {
179
                /** @var int $position */
180
                return substr_replace($sql, $replacement, $position, strlen($placeholder));
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr_replace($s..., strlen($placeholder)) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
181
            }
182
        }
183
184
        return $sql;
185
    }
186
187
    /**
188
     * Creates an instance of {@see SqlParser} for the given SQL statement.
189
     *
190
     * @param string $sql SQL statement to be parsed.
191
     *
192
     * @return SqlParser SQL parser instance.
193
     */
194
    protected function createSqlParser(string $sql): SqlParser
195
    {
196
        return new SqlParser($sql);
197
    }
198
}
199