Passed
Push — master ( e689ea...fbefdf )
by William
02:57
created

Key   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 100
dl 0
loc 264
ccs 84
cts 84
cp 1
rs 9.28
c 2
b 0
f 0
wmc 39

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
D parse() 0 137 31
B build() 0 32 7
1
<?php
2
/**
3
 * Parses the definition of a key.
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\SqlParser\Components;
9
10
use PhpMyAdmin\SqlParser\Component;
11
use PhpMyAdmin\SqlParser\Context;
12
use PhpMyAdmin\SqlParser\Parser;
13
use PhpMyAdmin\SqlParser\Token;
14
use PhpMyAdmin\SqlParser\TokensList;
15
16
use function implode;
17
use function trim;
18
19
/**
20
 * Parses the definition of a key.
21
 *
22
 * Used for parsing `CREATE TABLE` statement.
23
 */
24
class Key extends Component
25
{
26
    /**
27
     * All key options.
28
     *
29
     * @var array
30
     */
31
    public static $KEY_OPTIONS = [
32
        'KEY_BLOCK_SIZE' => [
33
            1,
34
            'var',
35
        ],
36
        'USING' => [
37
            2,
38
            'var',
39
        ],
40
        'WITH PARSER' => [
41
            3,
42
            'var',
43
        ],
44
        'COMMENT' => [
45
            4,
46
            'var',
47
        ],
48
    ];
49
50
    /**
51
     * The name of this key.
52
     *
53
     * @var string
54
     */
55
    public $name;
56
57
    /**
58
     * The key columns
59
     *
60
     * @var array[]
61
     * @phpstan-var array{name?: string, length?: int, order?: string}[]
62
     */
63
    public $columns;
64
65
    /**
66
     * The type of this key.
67
     *
68
     * @var string
69
     */
70
    public $type;
71
72
    /**
73
     * The expression if the Key is not using column names
74
     *
75
     * @var string|null
76
     */
77
    public $expr = null;
78
79
    /**
80
     * The options of this key or null if none where found.
81
     *
82
     * @var OptionsArray|null
83
     */
84
    public $options;
85
86
    /**
87
     * @param string       $name    the name of the key
88
     * @param array        $columns the columns covered by this key
89
     * @param string       $type    the type of this key
90
     * @param OptionsArray $options the options of this key
91
     */
92 116
    public function __construct(
93
        $name = null,
94
        array $columns = [],
95
        $type = null,
96
        $options = null
97
    ) {
98 116
        $this->name = $name;
99 116
        $this->columns = $columns;
100 116
        $this->type = $type;
101 116
        $this->options = $options;
102 116
    }
103
104
    /**
105
     * @param Parser     $parser  the parser that serves as context
106
     * @param TokensList $list    the list of tokens that are being parsed
107
     * @param array      $options parameters for parsing
108
     *
109
     * @return Key
110
     */
111 116
    public static function parse(Parser $parser, TokensList $list, array $options = [])
112
    {
113 116
        $ret = new static();
114
115
        /**
116
         * Last parsed column.
117
         *
118
         * @var array<string,mixed>
119
         */
120 116
        $lastColumn = [];
121
122
        /**
123
         * The state of the parser.
124
         *
125
         * Below are the states of the parser.
126
         *
127
         *      0 ---------------------[ type ]---------------------------> 1
128
         *
129
         *      1 ---------------------[ name ]---------------------------> 1
130
         *      1 ---------------------[ columns ]------------------------> 2
131
         *      1 ---------------------[ expression ]---------------------> 5
132
         *
133
         *      2 ---------------------[ column length ]------------------> 3
134
         *      3 ---------------------[ column length ]------------------> 2
135
         *      2 ---------------------[ options ]------------------------> 4
136
         *      5 ---------------------[ expression ]---------------------> 4
137
         *
138
         * @var int
139
         */
140 116
        $state = 0;
141
142 116
        for (; $list->idx < $list->count; ++$list->idx) {
143
            /**
144
             * Token parsed at this moment.
145
             *
146
             * @var Token
147
             */
148 116
            $token = $list->tokens[$list->idx];
149
150
            // End of statement.
151 116
            if ($token->type === Token::TYPE_DELIMITER) {
152 8
                break;
153
            }
154
155
            // Skipping whitespaces and comments.
156 112
            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
157 112
                continue;
158
            }
159
160 112
            if ($state === 0) {
161 112
                $ret->type = $token->value;
162 112
                $state = 1;
163 112
            } elseif ($state === 1) {
164 112
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
165 112
                    $positionBeforeSearch = $list->idx;
166 112
                    $list->idx++;// Ignore the current token "(" or the search condition will always be true
167 112
                    $nextToken = $list->getNext();
168 112
                    $list->idx = $positionBeforeSearch;// Restore the position
169
170 112
                    if ($nextToken !== null && $nextToken->value === '(') {
171
                        // Switch to expression mode
172 24
                        $state = 5;
173
                    } else {
174 112
                        $state = 2;
175
                    }
176
                } else {
177 112
                    $ret->name = $token->value;
178
                }
179 112
            } elseif ($state === 2) {
180 92
                if ($token->type === Token::TYPE_OPERATOR) {
181 92
                    if ($token->value === '(') {
182 24
                        $state = 3;
183 92
                    } elseif (($token->value === ',') || ($token->value === ')')) {
184 92
                        $state = $token->value === ',' ? 2 : 4;
185 92
                        if (! empty($lastColumn)) {
186 92
                            $ret->columns[] = $lastColumn;
187 92
                            $lastColumn = [];
188
                        }
189
                    }
190
                } elseif (
191
                    (
192 92
                        $token->type === Token::TYPE_KEYWORD
193
                    )
194
                    &&
195
                    (
196 92
                        ($token->keyword === 'ASC') || ($token->keyword === 'DESC')
197
                    )
198
                ) {
199 16
                    $lastColumn['order'] = $token->keyword;
200
                } else {
201 92
                    $lastColumn['name'] = $token->value;
202
                }
203 108
            } elseif ($state === 3) {
204 24
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ')')) {
205 24
                    $state = 2;
206
                } else {
207 24
                    $lastColumn['length'] = $token->value;
208
                }
209 108
            } elseif ($state === 4) {
210 108
                $ret->options = OptionsArray::parse($parser, $list, static::$KEY_OPTIONS);
211 108
                ++$list->idx;
212 108
                break;
213 24
            } elseif ($state === 5) {
214 24
                if ($token->type === Token::TYPE_OPERATOR) {
215
                    // This got back to here and we reached the end of the expression
216 24
                    if ($token->value === ')') {
217 24
                        $state = 4;// go back to state 4 to fetch options
218 24
                        continue;
219
                    }
220
221
                    // The expression is not finished, adding a separator for the next expression
222 24
                    if ($token->value === ',') {
223 12
                        $ret->expr .= ', ';
224 12
                        continue;
225
                    }
226
227
                    // Start of the expression
228 24
                    if ($token->value === '(') {
229
                        // This is the first expression, set to empty
230 24
                        if ($ret->expr === null) {
231 24
                            $ret->expr = '';
232
                        }
233
234 24
                        $ret->expr .= Expression::parse($parser, $list, ['parenthesesDelimited' => true]);
235 24
                        continue;
236
                    }
237
                    // Another unexpected operator was found
238
                }
239
240
                // Something else than an operator was found
241 4
                $parser->error('Unexpected token.', $token);
242
            }
243
        }
244
245 116
        --$list->idx;
246
247 116
        return $ret;
248
    }
249
250
    /**
251
     * @param Key   $component the component to be built
252
     * @param array $options   parameters for building
253
     *
254
     * @return string
255
     */
256 72
    public static function build($component, array $options = [])
257
    {
258 72
        $ret = $component->type . ' ';
259 72
        if (! empty($component->name)) {
260 60
            $ret .= Context::escape($component->name) . ' ';
261
        }
262
263 72
        if ($component->expr !== null) {
264 24
            return $ret . '(' . $component->expr . ') ' . $component->options;
265
        }
266
267 52
        $columns = [];
268 52
        foreach ($component->columns as $column) {
269 48
            $tmp = '';
270 48
            if (isset($column['name'])) {
271 48
                $tmp .= Context::escape($column['name']);
272
            }
273
274 48
            if (isset($column['length'])) {
275 20
                $tmp .= '(' . $column['length'] . ')';
276
            }
277
278 48
            if (isset($column['order'])) {
279 16
                $tmp .= ' ' . $column['order'];
280
            }
281
282 48
            $columns[] = $tmp;
283
        }
284
285 52
        $ret .= '(' . implode(',', $columns) . ') ' . $component->options;
286
287 52
        return trim($ret);
288
    }
289
}
290