Passed
Pull Request — master (#806)
by Sergei
11:39
created

ExpressionBuilder::createSqlParser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
ccs 0
cts 0
cp 0
crap 2
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
92
            $paramName = $name[0] === ':' ? substr($name, 1) : $name;
93
            $uniqueName = $this->getUniqueName($paramName, $params);
94
95
            $sql = $this->replacePlaceholder($sql, ":$paramName", ":$uniqueName");
96
97
            if ($name[0] === ':') {
98
                $uniqueName = ":$uniqueName";
99
            }
100
101
            $params[$uniqueName] = $value;
102
            $expressionParams[$uniqueName] = $value;
103
            unset($expressionParams[$name]);
104
        }
105
106
        return $sql;
107
    }
108
109
    /**
110
     * Replaces parameters with expression values in SQL statement.
111
     *
112
     * @param string $sql SQL statement where parameters should be replaced.
113
     * @param array $expressionParams Parameters to be replaced.
114
     * @param array $params Parameters to be bound to the query.
115
     *
116
     * @psalm-param ParamsType $expressionParams
117
     * @psalm-param ParamsType $params
118
     *
119
     * @return string SQL statement with replaced parameters.
120
     */
121
    private function replaceParamExpressions(string $sql, array $expressionParams, array &$params): string
122
    {
123
        /** @var non-empty-string $name */
124
        foreach ($expressionParams as $name => $value) {
125
            if (!$value instanceof ExpressionInterface) {
126
                continue;
127
            }
128
129
            $placeholder = $name[0] !== ':' ? ":$name" : $name;
130
            /** @psalm-suppress PossiblyNullReference */
131
            $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

131
            /** @scrutinizer ignore-call */ 
132
            $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...
132
133
            $sql = $this->replacePlaceholder($sql, $placeholder, $replacement);
134
135
            /** @psalm-var ParamsType $params */
136
            unset($params[$name]);
137
        }
138
139
        return $sql;
140
    }
141
142
    /**
143
     * Returns a unique name for the parameter without colon at the beginning.
144
     *
145
     * @param string $name Name of the parameter without colon at the beginning.
146
     * @param array $params Parameters to be bound to the query.
147
     *
148
     * @psalm-param ParamsType $params
149
     *
150
     * @return string Unique name of the parameter with colon at the beginning.
151
     *
152
     * @psalm-return non-empty-string
153
     */
154
    private function getUniqueName(string $name, array $params): string
155
    {
156
        $uniqueName = $name . '_0';
157
158
        for ($i = 1; isset($params[$uniqueName]) || isset($params[":$uniqueName"]); ++$i) {
159
            $uniqueName = $name . '_' . $i;
160
        }
161
162
        return $uniqueName;
163
    }
164
165
    /**
166
     * Replaces the placeholder with the replacement in SQL statement.
167
     *
168
     * @param string $sql SQL statement where the placeholder should be replaced.
169
     * @param string $placeholder Placeholder to be replaced.
170
     * @param string $replacement Replacement for the placeholder.
171
     *
172
     * @return string SQL with the replaced placeholder.
173
     */
174
    private function replacePlaceholder(string $sql, string $placeholder, string $replacement): string
175
    {
176
        $parser = $this->createSqlParser($sql);
177
178
        while (null !== $parsedPlaceholder = $parser->getNextPlaceholder($position)) {
179
            if ($parsedPlaceholder === $placeholder) {
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