GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

IndexDefinition   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 163
dl 0
loc 354
rs 3.6
c 4
b 0
f 0
ccs 167
cts 167
cp 1
wmc 60

12 Methods

Rating   Name   Duplication   Size   Complexity  
A parseOptionalIndexName() 0 8 2
A expectIndexColumns() 0 28 6
A parseIndexType() 0 10 3
A parseIndexColumns() 0 3 1
A parseIndexOptions() 0 14 6
B parseReferenceDefinition() 0 28 6
A parseOptionalIndexType() 0 4 2
A parseReferenceOption() 0 10 3
B parse() 0 35 8
F toString() 0 56 17
A getCovers() 0 21 3
A getColumns() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like IndexDefinition often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IndexDefinition, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Graze\Morphism\Parse;
3
4
use LogicException;
5
use RuntimeException;
6
7
/**
8
 * Represents the definition of an index.
9
 */
10
class IndexDefinition
11
{
12
    /** @var string|null */
13
    public $constraint = null;
14
15
    /** @var string */
16
    public $type = '';
17
18
    /** @var string|null */
19
    public $name = null;
20
21
    /**
22
     * @var array[] the columns which are indexed.
23
     *
24
     * Each entry is an array of the form:
25
     *
26
     * 'name'   => string
27
     * 'length' => int|null
28
     * 'sort'   => 'ASC'|'DESC'
29
     */
30
    public $columns = [];
31
32
    /** @var mixed[] Associative array of index options
33
     *
34
     * 'USING'          => string 'BTREE' | 'HASH'
35
     * 'KEY_BLOCK_SIZE' => integer
36
     * 'WITH PARSER'    => string;
37
     * 'COMMENT'        => string
38
     */
39
    public $options = [];
40
41
    /**
42
     * @var array Only present for FOREIGN KEYS
43
     *
44
     * 'table'     => string name of foreign table
45
     * 'columns'   => [ 'name' => string, 'length' => int|null, 'sort' => string][] columns referenced in foreign table
46
     * 'ON DELETE' => string 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'NO ACTION'
47
     * 'ON UPDATE' => string 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'NO ACTION'
48
     */
49
    public $reference = [];
50
51
    /**
52
     * Parses an index definition from $stream
53
     *
54
     * The type of key (PRIMARY KEY, UNIQUE KEY, etc) should already have
55
     * been parsed from the stream. If the optional preceding CONSTRAINT clause
56
     * was parsed, you should supply its optional name in $constraint.
57
     *
58
     * @param TokenStream $stream
59
     * @param string $type 'PRIMARY KEY' | 'UNIQUE KEY' | 'KEY' | 'FULLTEXT KEY' | 'FOREIGN KEY'
60
     * @param string|null $constraint name supplied in optional CONSTRAINT clause
61
     */
62 94
    public function parse(TokenStream $stream, $type, $constraint = null)
63
    {
64 94
        $this->type = $type;
65
66 94
        switch ($type) {
67 94
            case 'PRIMARY KEY':
68 12
                $this->parseOptionalIndexType($stream);
69 12
                $this->parseIndexColumns($stream);
70 12
                $this->parseIndexOptions($stream);
71 12
                break;
72
73 83
            case 'UNIQUE KEY':
74 75
            case 'KEY':
75 37
            case 'FULLTEXT KEY':
76 58
                $this->parseOptionalIndexType($stream);
77 57
                if (!isset($this->options['USING'])) {
78 56
                    $this->parseOptionalIndexName($stream);
79 56
                    $this->parseOptionalIndexType($stream);
80
                }
81 57
                if (!is_null($constraint)) {
82 2
                    $this->name = $constraint;
83
                }
84 57
                $this->parseIndexColumns($stream);
85 57
                $this->parseIndexOptions($stream);
86 57
                break;
87
88 31
            case 'FOREIGN KEY':
89 30
                $this->constraint = $constraint;
90 30
                $this->parseOptionalIndexName($stream);
91 30
                $this->parseIndexColumns($stream);
92 30
                $this->parseReferenceDefinition($stream);
93 28
                break;
94
95
            default:
96 1
                throw new LogicException("Internal error - unknown index type '$type'");
97
        }
98 90
    }
99
100
    /**
101
     * @param TokenStream $stream
102
     */
103 80
    private function parseOptionalIndexName(TokenStream $stream)
104
    {
105 80
        $mark = $stream->getMark();
106 80
        $token = $stream->nextToken();
107 80
        if ($token->type === Token::IDENTIFIER) {
108 17
            $this->name = $token->text;
109
        } else {
110 69
            $stream->rewind($mark);
111
        }
112 80
    }
113
114
    /**
115
     * @param TokenStream $stream
116
     */
117 69
    private function parseOptionalIndexType(TokenStream $stream)
118
    {
119 69
        if ($stream->consume('USING')) {
120 3
            $this->parseIndexType($stream);
121
        }
122 68
    }
123
124
    /**
125
     * @param TokenStream $stream
126
     */
127 6
    private function parseIndexType(TokenStream $stream)
128
    {
129 6
        if ($stream->consume('BTREE')) {
130 4
            $using = 'BTREE';
131 2
        } elseif ($stream->consume('HASH')) {
132 1
            $using = 'HASH';
133
        } else {
134 1
            throw new RuntimeException("Expected BTREE or HASH");
135
        }
136 5
        $this->options['USING'] = $using;
137 5
    }
138
139
    /**
140
     * @param TokenStream $stream
141
     */
142 92
    private function parseIndexColumns(TokenStream $stream)
143
    {
144 92
        $this->columns = $this->expectIndexColumns($stream);
145 92
    }
146
147
    /**
148
     * @param TokenStream $stream
149
     * @return array
150
     */
151 92
    private function expectIndexColumns(TokenStream $stream)
152
    {
153 92
        $columns = [];
154 92
        $stream->expect(Token::SYMBOL, '(');
155 92
        while (true) {
156
            $column = [
157 92
                'name'   => $stream->expectName(),
158
                'length' => null,
159 92
                'sort'   => 'ASC',
160
            ];
161 92
            if ($stream->consume([[Token::SYMBOL, '(']])) {
162 5
                $column['length'] = $stream->expectNumber();
163 5
                $stream->expect(Token::SYMBOL, ')');
164
            }
165 92
            if ($stream->consume('ASC')) {
166 1
                $column['sort'] = 'ASC';
167 91
            } elseif ($stream->consume('DESC')) {
168 1
                $column['sort'] = 'DESC';
169
            }
170 92
            $columns[] = $column;
171 92
            if (!$stream->consume([[Token::SYMBOL, ',']])) {
172 92
                break;
173
            }
174
        }
175
176 92
        $stream->expect(Token::SYMBOL, ')');
177
178 92
        return $columns;
179
    }
180
181
    /**
182
     * @param TokenStream $stream
183
     */
184 68
    private function parseIndexOptions(TokenStream $stream)
185
    {
186 68
        while (true) {
187 68
            if ($stream->consume('KEY_BLOCK_SIZE')) {
188 4
                $stream->consume([[Token::SYMBOL, '=']]);
189 4
                $this->options['KEY_BLOCK_SIZE'] = $stream->expectNumber();
190 68
            } elseif ($stream->consume('WITH PARSER')) {
191 1
                $this->options['WITH PARSER'] = $stream->expectName();
192 68
            } elseif ($stream->consume('COMMENT')) {
193 3
                $this->options['COMMENT'] = $stream->expectString();
194 68
            } elseif ($stream->consume('USING')) {
195 3
                $this->parseIndexType($stream);
196
            } else {
197 68
                break;
198
            }
199
        }
200 68
    }
201
202
    /**
203
     * @param TokenStream $stream
204
     */
205 30
    private function parseReferenceDefinition(TokenStream $stream)
206
    {
207 30
        $stream->expect(Token::IDENTIFIER, 'REFERENCES');
208
209 30
        $tableOrSchema = $stream->expectName();
210 30
        if ($stream->consume([[Token::SYMBOL, '.']])) {
211 1
            $schema = $tableOrSchema;
212 1
            $table = $stream->expectName();
213
        } else {
214 29
            $schema = null;
215 29
            $table = $tableOrSchema;
216
        }
217
218 30
        $this->reference['schema'] = $schema;
219 30
        $this->reference['table'] = $table;
220 30
        $this->reference['columns'] = $this->expectIndexColumns($stream);
221 30
        $this->reference['ON DELETE'] = 'RESTRICT';
222 30
        $this->reference['ON UPDATE'] = 'RESTRICT';
223
224 30
        while (true) {
225 30
            if ($stream->consume('MATCH')) {
226 1
                throw new RuntimeException("MATCH clause is not supported in this tool, or in MySQL itself!");
227 29
            } elseif ($stream->consume('ON DELETE')) {
228 5
                $this->parseReferenceOption($stream, 'ON DELETE');
229 29
            } elseif ($stream->consume('ON UPDATE')) {
230 6
                $this->parseReferenceOption($stream, 'ON UPDATE');
231
            } else {
232 28
                break;
233
            }
234
        }
235 28
    }
236
237
    /**
238
     * @param TokenStream $stream
239
     * @param string $clause
240
     */
241 10
    private function parseReferenceOption(TokenStream $stream, $clause)
242
    {
243 10
        $availableOptions = ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION'];
244 10
        foreach ($availableOptions as $option) {
245 10
            if ($stream->consume($option)) {
246 9
                $this->reference[$clause] = $option;
247 9
                return;
248
            }
249
        }
250 1
        throw new RuntimeException("Expected one of: " . implode(", ", $availableOptions));
251
    }
252
253
    /**
254
     * Returns an array of all sequences of columns covered by this index. Includes optional lengths.
255
     *
256
     * E.g. the index ```KEY idx (x,y(12),z)``` will give ```[['x', 'y(12)', 'z'], ['x', 'y(12)'], ['x']]```.
257
     *
258
     * @return string[][]
259
     */
260 46
    public function getCovers()
261
    {
262
        // TODO - we should really have separate IndexPart objects so we can
263
        // correctly cope with pathological cases like:
264
        //
265
        //      CREATE TABLE evil (
266
        //          `bad(16)` varchar(32) not null,
267
        //          KEY (`bad(16)`(8))
268
        //      )
269
        //
270 46
        $covers = [];
271 46
        $cover = [];
272 46
        foreach ($this->columns as $column) {
273 46
            $name = $column['name'];
274 46
            if ($column['length']) {
275 1
                $name .= '(' . $column['length'] . ')';
276
            }
277 46
            $cover[] = $name;
278 46
            $covers[] = $cover;
279
        }
280 46
        return $covers;
281
    }
282
283
    /**
284
     * Returns the sequence of indexed columns, including optional lengths.
285
     *
286
     * E.g. the index ```KEY idx (x, y(12))``` will give ```['x', 'y(12)']```
287
     *
288
     * @return string[]
289
     */
290 18
    public function getColumns()
291
    {
292 18
        $columns = [];
293 18
        foreach ($this->columns as $column) {
294 18
            $name = $column['name'];
295 18
            if ($column['length']) {
296 1
                $name .= '(' . $column['length'] . ')';
297
            }
298 18
            $columns[] = $name;
299
        }
300 18
        return $columns;
301
    }
302
303
    /**
304
     * Returns an SQL fragment for declaring this index as part of a table definition.
305
     *
306
     * @return string
307
     */
308 81
    public function toString()
309
    {
310 81
        $line = '';
311 81
        if ($this->type === 'FOREIGN KEY') {
312 26
            $line = "CONSTRAINT " . Token::escapeIdentifier($this->constraint) . " ";
313
        }
314 81
        $line .= $this->type;
315 81
        if (!in_array($this->type, ['PRIMARY KEY', 'FOREIGN KEY'])) {
316 53
            $line .= " " . Token::escapeIdentifier($this->name);
317
        }
318 81
        $cols = [];
319 81
        foreach ($this->columns as $column) {
320 81
            $col = Token::escapeIdentifier($column['name']);
321 81
            if (!is_null($column['length'])) {
322 2
                $col .= "(" . $column['length'] . ")";
323
            }
324 81
            $cols[] = $col;
325
        }
326 81
        $line .= " (" . implode(',', $cols) . ")";
327
328 81
        if (isset($this->options['USING'])) {
329 5
            $line .= " USING " . $this->options['USING'];
330
        }
331 81
        if (isset($this->options['KEY_BLOCK_SIZE']) && $this->options['KEY_BLOCK_SIZE'] !== 0) {
332 3
            $line .= " KEY_BLOCK_SIZE=" . $this->options['KEY_BLOCK_SIZE'];
333
        }
334 81
        if (isset($this->options['COMMENT']) && $this->options['COMMENT'] !== '') {
335 2
            $line .= " COMMENT " . Token::escapeString($this->options['COMMENT']);
336
        }
337 81
        if (isset($this->options['WITH PARSER'])) {
338 1
            $line .= " WITH PARSER " . $this->options['WITH PARSER'];
339
        }
340
341 81
        if ($this->type === 'FOREIGN KEY') {
342 26
            $reference = Token::escapeIdentifier($this->reference['table']);
343 26
            if (!is_null($this->reference['schema'])) {
344 1
                $reference = Token::escapeIdentifier($this->reference['schema']) . '.' . $reference;
345
            }
346 26
            $line .= " REFERENCES $reference";
347 26
            $cols = [];
348 26
            foreach ($this->reference['columns'] as $column) {
349 26
                $col = Token::escapeIdentifier($column['name']);
350 26
                if (!is_null($column['length'])) {
351 1
                    $col .= "(" . $column['length'] . ")";
352
                }
353 26
                $cols[] = $col;
354
            }
355 26
            $line .= " (" . implode(',', $cols) . ")";
356 26
            foreach (['ON DELETE', 'ON UPDATE'] as $clause) {
357 26
                $action = $this->reference[$clause];
358 26
                if ($action !== 'RESTRICT') {
359 7
                    $line .= " $clause $action";
360
                }
361
            }
362
        }
363 81
        return $line;
364
    }
365
}
366