SqlToken::tokensMatch()   C
last analyzed

Complexity

Conditions 13
Paths 12

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 13.0096

Importance

Changes 0
Metric Value
cc 13
eloc 26
c 0
b 0
f 0
nc 12
nop 5
dl 0
loc 46
ccs 25
cts 26
cp 0.9615
crap 13.0096
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use yii\base\BaseObject;
11
12
/**
13
 * SqlToken represents SQL tokens produced by [[SqlTokenizer]] or its child classes.
14
 *
15
 * @property SqlToken[] $children Child tokens.
16
 * @property-read bool $hasChildren Whether the token has children.
17
 * @property-read bool $isCollection Whether the token represents a collection of tokens.
18
 * @property-read string $sql SQL code.
19
 *
20
 * @author Sergey Makinen <[email protected]>
21
 * @since 2.0.13
22
 */
23
class SqlToken extends BaseObject implements \ArrayAccess
24
{
25
    const TYPE_CODE = 0;
26
    const TYPE_STATEMENT = 1;
27
    const TYPE_TOKEN = 2;
28
    const TYPE_PARENTHESIS = 3;
29
    const TYPE_KEYWORD = 4;
30
    const TYPE_OPERATOR = 5;
31
    const TYPE_IDENTIFIER = 6;
32
    const TYPE_STRING_LITERAL = 7;
33
34
    /**
35
     * @var int token type. It has to be one of the following constants:
36
     *
37
     * - [[TYPE_CODE]]
38
     * - [[TYPE_STATEMENT]]
39
     * - [[TYPE_TOKEN]]
40
     * - [[TYPE_PARENTHESIS]]
41
     * - [[TYPE_KEYWORD]]
42
     * - [[TYPE_OPERATOR]]
43
     * - [[TYPE_IDENTIFIER]]
44
     * - [[TYPE_STRING_LITERAL]]
45
     */
46
    public $type = self::TYPE_TOKEN;
47
    /**
48
     * @var string|null token content.
49
     */
50
    public $content;
51
    /**
52
     * @var int original SQL token start position.
53
     */
54
    public $startOffset;
55
    /**
56
     * @var int original SQL token end position.
57
     */
58
    public $endOffset;
59
    /**
60
     * @var SqlToken parent token.
61
     */
62
    public $parent;
63
64
    /**
65
     * @var SqlToken[] token children.
66
     */
67
    private $_children = [];
68
69
70
    /**
71
     * Returns the SQL code representing the token.
72
     * @return string SQL code.
73
     */
74
    public function __toString()
75
    {
76
        return $this->getSql();
77
    }
78
79
    /**
80
     * Returns whether there is a child token at the specified offset.
81
     * This method is required by the SPL [[\ArrayAccess]] interface.
82
     * It is implicitly called when you use something like `isset($token[$offset])`.
83
     * @param int $offset child token offset.
84
     * @return bool whether the token exists.
85
     */
86 12
    #[\ReturnTypeWillChange]
87
    public function offsetExists($offset)
88
    {
89 12
        return isset($this->_children[$this->calculateOffset($offset)]);
90
    }
91
92
    /**
93
     * Returns a child token at the specified offset.
94
     * This method is required by the SPL [[\ArrayAccess]] interface.
95
     * It is implicitly called when you use something like `$child = $token[$offset];`.
96
     * @param int $offset child token offset.
97
     * @return SqlToken|null the child token at the specified offset, `null` if there's no token.
98
     */
99 25
    #[\ReturnTypeWillChange]
100
    public function offsetGet($offset)
101
    {
102 25
        $offset = $this->calculateOffset($offset);
103 25
        return isset($this->_children[$offset]) ? $this->_children[$offset] : null;
104
    }
105
106
    /**
107
     * Adds a child token to the token.
108
     * This method is required by the SPL [[\ArrayAccess]] interface.
109
     * It is implicitly called when you use something like `$token[$offset] = $child;`.
110
     * @param int|null $offset child token offset.
111
     * @param SqlToken $token token to be added.
112
     */
113 25
    #[\ReturnTypeWillChange]
114
    public function offsetSet($offset, $token)
115
    {
116 25
        $token->parent = $this;
117 25
        if ($offset === null) {
118 25
            $this->_children[] = $token;
119
        } else {
120
            $this->_children[$this->calculateOffset($offset)] = $token;
121
        }
122 25
        $this->updateCollectionOffsets();
123
    }
124
125
    /**
126
     * Removes a child token at the specified offset.
127
     * This method is required by the SPL [[\ArrayAccess]] interface.
128
     * It is implicitly called when you use something like `unset($token[$offset])`.
129
     * @param int $offset child token offset.
130
     */
131 13
    #[\ReturnTypeWillChange]
132
    public function offsetUnset($offset)
133
    {
134 13
        $offset = $this->calculateOffset($offset);
135 13
        if (isset($this->_children[$offset])) {
136 13
            array_splice($this->_children, $offset, 1);
137
        }
138 13
        $this->updateCollectionOffsets();
139
    }
140
141
    /**
142
     * Returns child tokens.
143
     * @return SqlToken[] child tokens.
144
     */
145 24
    public function getChildren()
146
    {
147 24
        return $this->_children;
148
    }
149
150
    /**
151
     * Sets a list of child tokens.
152
     * @param SqlToken[] $children child tokens.
153
     */
154
    public function setChildren($children)
155
    {
156
        $this->_children = [];
157
        foreach ($children as $child) {
158
            $child->parent = $this;
159
            $this->_children[] = $child;
160
        }
161
        $this->updateCollectionOffsets();
162
    }
163
164
    /**
165
     * Returns whether the token represents a collection of tokens.
166
     * @return bool whether the token represents a collection of tokens.
167
     */
168 25
    public function getIsCollection()
169
    {
170 25
        return in_array($this->type, [
171 25
            self::TYPE_CODE,
172 25
            self::TYPE_STATEMENT,
173 25
            self::TYPE_PARENTHESIS,
174 25
        ], true);
175
    }
176
177
    /**
178
     * Returns whether the token represents a collection of tokens and has non-zero number of children.
179
     * @return bool whether the token has children.
180
     */
181 25
    public function getHasChildren()
182
    {
183 25
        return $this->getIsCollection() && !empty($this->_children);
184
    }
185
186
    /**
187
     * Returns the SQL code representing the token.
188
     * @return string SQL code.
189
     */
190 15
    public function getSql()
191
    {
192 15
        $code = $this;
193 15
        while ($code->parent !== null) {
194 15
            $code = $code->parent;
195
        }
196
197 15
        return mb_substr($code->content, $this->startOffset, $this->endOffset - $this->startOffset, 'UTF-8');
0 ignored issues
show
Bug introduced by
It seems like $code->content can also be of type null; however, parameter $string of mb_substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

197
        return mb_substr(/** @scrutinizer ignore-type */ $code->content, $this->startOffset, $this->endOffset - $this->startOffset, 'UTF-8');
Loading history...
198
    }
199
200
    /**
201
     * Returns whether this token (including its children) matches the specified "pattern" SQL code.
202
     *
203
     * Usage Example:
204
     *
205
     * ```php
206
     * $patternToken = (new \yii\db\sqlite\SqlTokenizer('SELECT any FROM any'))->tokenize();
207
     * if ($sqlToken->matches($patternToken, 0, $firstMatchIndex, $lastMatchIndex)) {
208
     *     // ...
209
     * }
210
     * ```
211
     *
212
     * @param SqlToken $patternToken tokenized SQL code to match against. In addition to normal SQL, the
213
     * `any` keyword is supported which will match any number of keywords, identifiers, whitespaces.
214
     * @param int $offset token children offset to start lookup with.
215
     * @param int|null $firstMatchIndex token children offset where a successful match begins.
216
     * @param int|null $lastMatchIndex token children offset where a successful match ends.
217
     * @return bool whether this token matches the pattern SQL code.
218
     */
219 12
    public function matches(SqlToken $patternToken, $offset = 0, &$firstMatchIndex = null, &$lastMatchIndex = null)
220
    {
221 12
        if (!$patternToken->getHasChildren()) {
222
            return false;
223
        }
224
225 12
        $patternToken = $patternToken[0];
226 12
        return $this->tokensMatch($patternToken, $this, $offset, $firstMatchIndex, $lastMatchIndex);
0 ignored issues
show
Bug introduced by
It seems like $patternToken can also be of type null; however, parameter $patternToken of yii\db\SqlToken::tokensMatch() does only seem to accept yii\db\SqlToken, maybe add an additional type check? ( Ignorable by Annotation )

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

226
        return $this->tokensMatch(/** @scrutinizer ignore-type */ $patternToken, $this, $offset, $firstMatchIndex, $lastMatchIndex);
Loading history...
227
    }
228
229
    /**
230
     * Tests the given token to match the specified pattern token.
231
     * @param SqlToken $patternToken
232
     * @param SqlToken $token
233
     * @param int $offset
234
     * @param int|null $firstMatchIndex
235
     * @param int|null $lastMatchIndex
236
     * @return bool
237
     */
238 12
    private function tokensMatch(SqlToken $patternToken, SqlToken $token, $offset = 0, &$firstMatchIndex = null, &$lastMatchIndex = null)
239
    {
240
        if (
241 12
            $patternToken->getIsCollection() !== $token->getIsCollection()
242 12
            || (!$patternToken->getIsCollection() && $patternToken->content !== $token->content)
243
        ) {
244 12
            return false;
245
        }
246
247 12
        if ($patternToken->children === $token->children) {
248 12
            $firstMatchIndex = $lastMatchIndex = $offset;
249 12
            return true;
250
        }
251
252 12
        $firstMatchIndex = $lastMatchIndex = null;
253 12
        $wildcard = false;
254 12
        for ($index = 0, $count = count($patternToken->children); $index < $count; $index++) {
255
            // Here we iterate token by token with an exception of "any" that toggles
256
            // an iteration until we matched with a next pattern token or EOF.
257 12
            if ($patternToken[$index]->content === 'any') {
258 12
                $wildcard = true;
259 12
                continue;
260
            }
261
262 12
            for ($limit = $wildcard ? count($token->children) : $offset + 1; $offset < $limit; $offset++) {
263 12
                if (!$wildcard && !isset($token[$offset])) {
264
                    break;
265
                }
266
267 12
                if (!$this->tokensMatch($patternToken[$index], $token[$offset])) {
0 ignored issues
show
Bug introduced by
It seems like $patternToken[$index] can also be of type null; however, parameter $patternToken of yii\db\SqlToken::tokensMatch() does only seem to accept yii\db\SqlToken, maybe add an additional type check? ( Ignorable by Annotation )

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

267
                if (!$this->tokensMatch(/** @scrutinizer ignore-type */ $patternToken[$index], $token[$offset])) {
Loading history...
Bug introduced by
It seems like $token[$offset] can also be of type null; however, parameter $token of yii\db\SqlToken::tokensMatch() does only seem to accept yii\db\SqlToken, maybe add an additional type check? ( Ignorable by Annotation )

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

267
                if (!$this->tokensMatch($patternToken[$index], /** @scrutinizer ignore-type */ $token[$offset])) {
Loading history...
268 12
                    continue;
269
                }
270
271 12
                if ($firstMatchIndex === null) {
272 12
                    $firstMatchIndex = $offset;
273
                }
274 12
                $lastMatchIndex = $offset;
275 12
                $wildcard = false;
276 12
                $offset++;
277 12
                continue 2;
278
            }
279
280 12
            return false;
281
        }
282
283 12
        return true;
284
    }
285
286
    /**
287
     * Returns an absolute offset in the children array.
288
     * @param int $offset
289
     * @return int
290
     */
291 25
    private function calculateOffset($offset)
292
    {
293 25
        if ($offset >= 0) {
294 25
            return $offset;
295
        }
296
297 25
        return count($this->_children) + $offset;
298
    }
299
300
    /**
301
     * Updates token SQL code start and end offsets based on its children.
302
     */
303 25
    private function updateCollectionOffsets()
304
    {
305 25
        if (!empty($this->_children)) {
306 25
            $this->startOffset = reset($this->_children)->startOffset;
307 25
            $this->endOffset = end($this->_children)->endOffset;
308
        }
309 25
        if ($this->parent !== null) {
310 25
            $this->parent->updateCollectionOffsets();
311
        }
312
    }
313
}
314