Completed
Push — master ( 415d44...ad0e75 )
by Michal
04:47
created

Expression   D

Complexity

Total Complexity 83

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.36%

Importance

Changes 17
Bugs 5 Features 2
Metric Value
wmc 83
c 17
b 5
f 2
lcom 1
cbo 5
dl 0
loc 411
ccs 180
cts 183
cp 0.9836
rs 4.8717

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
F parse() 0 250 70
D build() 0 28 10

How to fix   Complexity   

Complex Class

Complex classes like Expression 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Expression, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Parses a reference to an expression (column, table or database name, function
5
 * call, mathematical expression, etc.).
6
 *
7
 * @package    SqlParser
8
 * @subpackage Components
9
 */
10
namespace SqlParser\Components;
11
12
use SqlParser\Context;
13
use SqlParser\Component;
14
use SqlParser\Parser;
15
use SqlParser\Token;
16
use SqlParser\TokensList;
17
18
/**
19
 * Parses a reference to an expression (column, table or database name, function
20
 * call, mathematical expression, etc.).
21
 *
22
 * @category   Components
23
 * @package    SqlParser
24
 * @subpackage Components
25
 * @author     Dan Ungureanu <[email protected]>
26
 * @license    http://opensource.org/licenses/GPL-2.0 GNU Public License
27
 */
28
class Expression extends Component
29
{
30
31
    /**
32
     * List of allowed reserved keywords in expressions.
33
     *
34
     * @var array
35
     */
36
    private static $ALLOWED_KEYWORDS = array(
37
        'AS' => 1, 'DUAL' => 1, 'NULL' => 1, 'REGEXP' => 1
38
    );
39
40
    /**
41
     * The name of this database.
42
     *
43
     * @var string
44
     */
45
    public $database;
46
47
    /**
48
     * The name of this table.
49
     *
50
     * @var string
51
     */
52
    public $table;
53
54
    /**
55
     * The name of the column.
56
     *
57
     * @var string
58
     */
59
    public $column;
60
61
    /**
62
     * The sub-expression.
63
     *
64
     * @var string
65
     */
66
    public $expr = '';
67
68
    /**
69
     * The alias of this expression.
70
     *
71
     * @var string
72
     */
73
    public $alias;
74
75
    /**
76
     * The name of the function.
77
     *
78
     * @var mixed
79
     */
80
    public $function;
81
82
    /**
83
     * The type of subquery.
84
     *
85
     * @var string
86
     */
87
    public $subquery;
88
89
    /**
90
     * Constructor.
91
     *
92
     * Syntax:
93
     *     new Expression('expr')
94
     *     new Expression('expr', 'alias')
95
     *     new Expression('database', 'table', 'column')
96
     *     new Expression('database', 'table', 'column', 'alias')
97
     *
98
     * If the database, table or column name is not required, pass an empty
99
     * string.
100
     *
101
     * @param string $database The name of the database or the the expression.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $database not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
102
     *                          the the expression.
103
     * @param string $table    The name of the table or the alias of the expression.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $table not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
104
     *                          the alias of the expression.
105
     * @param string $column   The name of the column.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $column not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
106
     * @param string $alias    The name of the alias.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $alias not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
107
     */
108 142
    public function __construct($database = null, $table = null, $column = null, $alias = null)
109
    {
110 142
        if (($column === null) && ($alias === null)) {
111 142
            $this->expr = $database; // case 1
112 142
            $this->alias = $table; // case 2
113 142
        } else {
114 2
            $this->database = $database; // case 3
115 2
            $this->table = $table; // case 3
116 2
            $this->column = $column; // case 3
117 2
            $this->alias = $alias; // case 4
118
        }
119 142
    }
120
121
    /**
122
     * Possible options:
123
     *
124
     *      `field`
125
     *
126
     *          First field to be filled.
127
     *          If this is not specified, it takes the value of `parseField`.
128
     *
129
     *      `parseField`
130
     *
131
     *          Specifies the type of the field parsed. It may be `database`,
132
     *          `table` or `column`. These expressions may not include
133
     *          parentheses.
134
     *
135
     *      `breakOnAlias`
136
     *
137
     *          If not empty, breaks when the alias occurs (it is not included).
138
     *
139
     *      `breakOnParentheses`
140
     *
141
     *          If not empty, breaks when the first parentheses occurs.
142
     *
143
     *      `parenthesesDelimited`
144
     *
145
     *          If not empty, breaks after last parentheses occurred.
146
     *
147
     * @param Parser     $parser  The parser that serves as context.
148
     * @param TokensList $list    The list of tokens that are being parsed.
149
     * @param array      $options Parameters for parsing.
150
     *
151
     * @return Expression
0 ignored issues
show
Documentation introduced by
Should the return type not be Expression|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
152
     */
153 137
    public static function parse(Parser $parser, TokensList $list, array $options = array())
154
    {
155 137
        $ret = new Expression();
156
157
        /**
158
         * Whether current tokens make an expression or a table reference.
159
         *
160
         * @var bool $isExpr
161
         */
162 137
        $isExpr = false;
163
164
        /**
165
         * Whether a period was previously found.
166
         *
167
         * @var bool $dot
168
         */
169 137
        $dot = false;
170
171
        /**
172
         * Whether an alias is expected. Is 2 if `AS` keyword was found.
173
         *
174
         * @var bool $alias
175
         */
176 137
        $alias = false;
177
178
        /**
179
         * Counts brackets.
180
         *
181
         * @var int $brackets
182
         */
183 137
        $brackets = 0;
184
185
        /**
186
         * Keeps track of the last two previous tokens.
187
         *
188
         * @var Token[] $prev
189
         */
190 137
        $prev = array(null, null);
191
192
        // When a field is parsed, no parentheses are expected.
193 137
        if (!empty($options['parseField'])) {
194 115
            $options['breakOnParentheses'] = true;
195 115
            $options['field'] = $options['parseField'];
196 115
        }
197
198 137
        for (; $list->idx < $list->count; ++$list->idx) {
199
200
            /**
201
             * Token parsed at this moment.
202
             *
203
             * @var Token $token
204
             */
205 137
            $token = $list->tokens[$list->idx];
206
207
            // End of statement.
208 137
            if ($token->type === Token::TYPE_DELIMITER) {
209 47
                break;
210
            }
211
212
            // Skipping whitespaces and comments.
213 136
            if (($token->type === Token::TYPE_WHITESPACE)
214 136
                || ($token->type === Token::TYPE_COMMENT)
215 136
            ) {
216 92
                if ($isExpr) {
217 50
                    $ret->expr .= $token->token;
218 50
                }
219 92
                continue;
220
            }
221
222 136
            if ($token->type === Token::TYPE_KEYWORD) {
223 81
                if (($brackets > 0) && (empty($ret->subquery))
224 81
                    && (!empty(Parser::$STATEMENT_PARSERS[$token->value]))
225 81
                ) {
226
                    // A `(` was previously found and this keyword is the
227
                    // beginning of a statement, so this is a subquery.
228 6
                    $ret->subquery = $token->value;
229 81
                } elseif (($token->flags & Token::FLAG_KEYWORD_FUNCTION)
230 80
                    && (empty($options['parseField']))
231 80
                ) {
232 13
                    $isExpr = true;
233 80
                } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED)
234 76
                    && ($brackets === 0)
235 76
                ) {
236 75
                    if (empty(self::$ALLOWED_KEYWORDS[$token->value])) {
237
                        // A reserved keyword that is not allowed in the
238
                        // expression was found so the expression must have
239
                        // ended and a new clause is starting.
240 70
                        break;
241
                    }
242 15
                    if ($token->value === 'AS') {
243 9
                        if (!empty($options['breakOnAlias'])) {
244 1
                            break;
245
                        }
246 8
                        if ($alias) {
247 1
                            $parser->error(
248 1
                                __('An alias was expected.'),
249
                                $token
250 1
                            );
251 1
                            break;
252
                        }
253 8
                        $alias = true;
254 8
                        continue;
255
                    }
256 6
                    $isExpr = true;
257 10
                } elseif ($brackets === 0 && count($ret->expr) > 0) {
258
                    /* End of expression */
259 3
                    break;
260
                }
261 23
            }
262
263 134
            if (($token->type === Token::TYPE_NUMBER)
264 130
                || ($token->type === Token::TYPE_BOOL)
265 130
                || (($token->type === Token::TYPE_SYMBOL)
266 130
                && ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
267 130
                || (($token->type === Token::TYPE_OPERATOR)
268 130
                && ($token->value !== '.'))
269 134
            ) {
270 98
                if (!empty($options['parseField'])) {
271 47
                    break;
272
                }
273
274
                // Numbers, booleans and operators (except dot) are usually part
275
                // of expressions.
276 68
                $isExpr = true;
277 68
            }
278
279 134
            if ($token->type === Token::TYPE_OPERATOR) {
280 64
                if ((!empty($options['breakOnParentheses']))
281 64
                    && (($token->value === '(') || ($token->value === ')'))
282 64
                ) {
283
                    // No brackets were expected.
284 2
                    break;
285
                }
286 63
                if ($token->value === '(') {
287 21
                    ++$brackets;
288 21
                    if ((empty($ret->function)) && ($prev[1] !== null)
289 21
                        && (($prev[1]->type === Token::TYPE_NONE)
290 7
                        || ($prev[1]->type === Token::TYPE_SYMBOL)
291 7
                        || (($prev[1]->type === Token::TYPE_KEYWORD)
292 7
                        && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
293 21
                    ) {
294 7
                        $ret->function = $prev[1]->value;
295 7
                    }
296 63
                } elseif ($token->value === ')') {
297 24
                    --$brackets;
298 24
                    if ($brackets === 0) {
299 21
                        if (!empty($options['parenthesesDelimited'])) {
300
                            // The current token is the last bracket, the next
301
                            // one will be outside the expression.
302 7
                            $ret->expr .= $token->token;
303 7
                            ++$list->idx;
304 7
                            break;
305
                        }
306 19
                    } elseif ($brackets < 0) {
307
                        // $parser->error(__('Unexpected closing bracket.'), $token);
308
                        // $brackets = 0;
309 3
                        break;
310
                    }
311 57
                } elseif ($token->value === ',') {
312
                    // Expressions are comma-delimited.
313 29
                    if ($brackets === 0) {
314 25
                        break;
315
                    }
316 4
                }
317 49
            }
318
319
            // Saving the previous tokens.
320 133
            $prev[0] = $prev[1];
321 133
            $prev[1] = $token;
322
323 133
            if ($alias) {
324
                // An alias is expected (the keyword `AS` was previously found).
325 7
                if (!empty($ret->alias)) {
326
                    $parser->error(__('An alias was previously found.'), $token);
327
                    break;
328
                }
329 7
                $ret->alias = $token->value;
330 7
                $alias = false;
331 133
            } elseif ($isExpr) {
332
                // Handling aliases.
333 62
                if (/* (empty($ret->alias)) && */ ($brackets === 0)
334 62
                    && (($prev[0] === null)
335 20
                    || ((($prev[0]->type !== Token::TYPE_OPERATOR)
336 10
                    || ($prev[0]->token === ')'))
337 20
                    && (($prev[0]->type !== Token::TYPE_KEYWORD)
338 20
                    || (!($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED)))))
339 62
                    && (($prev[1]->type === Token::TYPE_STRING)
340 54
                    || (($prev[1]->type === Token::TYPE_SYMBOL)
341 54
                    && (!($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE)))
342 54
                    || ($prev[1]->type === Token::TYPE_NONE))
343 62
                ) {
344 2
                    if (!empty($ret->alias)) {
345 1
                        $parser->error(__('An alias was previously found.'), $token);
346 1
                        break;
347
                    }
348 2
                    $ret->alias = $prev[1]->value;
349 2
                } else {
350 62
                    $ret->expr .= $token->token;
351
                }
352 133
            } elseif (!$isExpr) {
353 118
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '.')) {
354
                    // Found a `.` which means we expect a column name and
355
                    // the column name we parsed is actually the table name
356
                    // and the table name is actually a database name.
357 13
                    if ((!empty($ret->database)) || ($dot)) {
358 2
                        $parser->error(__('Unexpected dot.'), $token);
359 2
                    }
360 13
                    $ret->database = $ret->table;
361 13
                    $ret->table = $ret->column;
362 13
                    $ret->column = null;
363 13
                    $dot = true;
364 13
                    $ret->expr .= $token->token;
365 13
                } else {
366 118
                    $field = empty($options['field']) ? 'column' : $options['field'];
367 118
                    if (empty($ret->$field)) {
368 118
                        $ret->$field = $token->value;
369 118
                        $ret->expr .= $token->token;
370 118
                        $dot = false;
371 118
                    } else {
372
                        // No alias is expected.
373 6
                        if (!empty($options['breakOnAlias'])) {
374
                            break;
375
                        }
376 6
                        if (!empty($ret->alias)) {
377 1
                            $parser->error(__('An alias was previously found.'), $token);
378 1
                            break;
379
                        }
380 6
                        $ret->alias = $token->value;
381
                    }
382
                }
383 118
            }
384 133
        }
385
386 137
        if ($alias) {
387 2
            $parser->error(
388 2
                __('An alias was expected.'),
389 2
                $list->tokens[$list->idx - 1]
390 2
            );
391 2
        }
392
393
        // White-spaces might be added at the end.
394 137
        $ret->expr = trim($ret->expr);
395
396 137
        if (empty($ret->expr)) {
397 11
            return null;
398
        }
399
400 133
        --$list->idx;
401 133
        return $ret;
402
    }
403
404
    /**
405
     * @param Expression|Expression[] $component The component to be built.
406
     * @param array                   $options   Parameters for building.
407
     *
408
     * @return string
409
     */
410 28
    public static function build($component, array $options = array())
411
    {
412 28
        if (is_array($component)) {
413 1
            return implode($component, ', ');
414
        } else {
415 28
            if (!empty($component->expr)) {
416 22
                $ret = $component->expr;
417 22
            } else {
418 8
                $fields = array();
419 8
                if ((isset($component->database)) && ($component->database !== '')) {
420 1
                    $fields[] = $component->database;
421 1
                }
422 8
                if ((isset($component->table)) && ($component->table !== '')) {
423 8
                    $fields[] = $component->table;
424 8
                }
425 8
                if ((isset($component->column)) && ($component->column !== '')) {
426 1
                    $fields[] = $component->column;
427 1
                }
428 8
                $ret = implode('.', Context::escape($fields));
429
            }
430
431 28
            if (!empty($component->alias)) {
432 2
                $ret .= ' AS ' . Context::escape($component->alias);
433 2
            }
434
435 28
            return $ret;
436
        }
437
    }
438
}
439