Completed
Push — master ( d3c22e...1ec169 )
by Michal
06:38
created

Expression   D

Complexity

Total Complexity 88

Size/Duplication

Total Lines 415
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.85%

Importance

Changes 17
Bugs 4 Features 1
Metric Value
wmc 88
c 17
b 4
f 1
lcom 1
cbo 5
dl 0
loc 415
ccs 182
cts 186
cp 0.9785
rs 4.8717

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
F parse() 0 254 74
C build() 0 28 11

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