Completed
Push — master ( a6ddea...8de474 )
by Michal
03:57
created

Expression::parse()   F

Complexity

Conditions 72
Paths 3848

Size

Total Lines 253
Code Lines 134

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 150
CRAP Score 72.0911

Importance

Changes 15
Bugs 3 Features 2
Metric Value
c 15
b 3
f 2
dl 0
loc 253
ccs 150
cts 154
cp 0.974
rs 2
cc 72
eloc 134
nc 3848
nop 3
crap 72.0911

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