/** * @param Parser $parser The parser that serves as context. * @param TokensList $list The list of tokens that are being parsed. * @param array $options Parameters for parsing. * * @return Expression[] */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = array(); /** * The state of the parser. * * Below are the states of the parser. * * 0 ----------------------[ array ]---------------------> 1 * * 1 ------------------------[ , ]------------------------> 0 * 1 -----------------------[ else ]----------------------> (END) * * @var int $state */ $state = 0; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $token */ $token = $list->tokens[$list->idx]; // End of statement. if ($token->type === Token::TYPE_DELIMITER) { break; } // Skipping whitespaces and comments. if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) { continue; } if ($token->type === Token::TYPE_KEYWORD && $token->flags & Token::FLAG_KEYWORD_RESERVED && ~$token->flags & Token::FLAG_KEYWORD_FUNCTION && $token->value !== 'DUAL' && $token->value !== 'NULL' && $token->value !== 'CASE') { // No keyword is expected. break; } if ($state === 0) { if ($token->type === Token::TYPE_KEYWORD && $token->value === 'CASE') { $expr = CaseExpression::parse($parser, $list, $options); } else { $expr = Expression::parse($parser, $list, $options); } if ($expr === null) { break; } $ret[] = $expr; $state = 1; } elseif ($state === 1) { if ($token->value === ',') { $state = 0; } else { break; } } } if ($state === 0) { $parser->error(__('An expression was expected.'), $list->tokens[$list->idx]); } --$list->idx; return $ret; }
/** * Possible options: * * `field` * * First field to be filled. * If this is not specified, it takes the value of `parseField`. * * `parseField` * * Specifies the type of the field parsed. It may be `database`, * `table` or `column`. These expressions may not include * parentheses. * * `breakOnAlias` * * If not empty, breaks when the alias occurs (it is not included). * * `breakOnParentheses` * * If not empty, breaks when the first parentheses occurs. * * `parenthesesDelimited` * * If not empty, breaks after last parentheses occurred. * * @param Parser $parser The parser that serves as context. * @param TokensList $list The list of tokens that are being parsed. * @param array $options Parameters for parsing. * * @return Expression */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new Expression(); /** * Whether current tokens make an expression or a table reference. * * @var bool $isExpr */ $isExpr = false; /** * Whether a period was previously found. * * @var bool $dot */ $dot = false; /** * Whether an alias is expected. Is 2 if `AS` keyword was found. * * @var bool $alias */ $alias = false; /** * Counts brackets. * * @var int $brackets */ $brackets = 0; /** * Keeps track of the last two previous tokens. * * @var Token[] $prev */ $prev = array(null, null); // When a field is parsed, no parentheses are expected. if (!empty($options['parseField'])) { $options['breakOnParentheses'] = true; $options['field'] = $options['parseField']; } for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $token */ $token = $list->tokens[$list->idx]; // End of statement. if ($token->type === Token::TYPE_DELIMITER) { break; } // Skipping whitespaces and comments. if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) { if ($isExpr) { $ret->expr .= $token->token; } continue; } if ($token->type === Token::TYPE_KEYWORD) { if ($brackets > 0 && empty($ret->subquery) && !empty(Parser::$STATEMENT_PARSERS[$token->value])) { // A `(` was previously found and this keyword is the // beginning of a statement, so this is a subquery. $ret->subquery = $token->value; } elseif ($token->flags & Token::FLAG_KEYWORD_FUNCTION && (empty($options['parseField']) && !$alias)) { $isExpr = true; } elseif ($token->flags & Token::FLAG_KEYWORD_RESERVED && $brackets === 0) { if (empty(self::$ALLOWED_KEYWORDS[$token->value])) { // A reserved keyword that is not allowed in the // expression was found so the expression must have // ended and a new clause is starting. break; } if ($token->value === 'AS') { if (!empty($options['breakOnAlias'])) { break; } if ($alias) { $parser->error(__('An alias was expected.'), $token); break; } $alias = true; continue; } elseif ($token->value === 'CASE') { // For a use of CASE like // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...' $tempCaseExpr = CaseExpression::parse($parser, $list); $ret->expr .= CaseExpression::build($tempCaseExpr); $isExpr = true; continue; } $isExpr = true; } elseif ($brackets === 0 && count($ret->expr) > 0 && !$alias) { /* End of expression */ break; } } if ($token->type === Token::TYPE_NUMBER || $token->type === Token::TYPE_BOOL || $token->type === Token::TYPE_SYMBOL && $token->flags & Token::FLAG_SYMBOL_VARIABLE || $token->type === Token::TYPE_OPERATOR && $token->value !== '.') { if (!empty($options['parseField'])) { break; } // Numbers, booleans and operators (except dot) are usually part // of expressions. $isExpr = true; } if ($token->type === Token::TYPE_OPERATOR) { if (!empty($options['breakOnParentheses']) && ($token->value === '(' || $token->value === ')')) { // No brackets were expected. break; } if ($token->value === '(') { ++$brackets; if (empty($ret->function) && $prev[1] !== null && ($prev[1]->type === Token::TYPE_NONE || $prev[1]->type === Token::TYPE_SYMBOL || $prev[1]->type === Token::TYPE_KEYWORD && $prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)) { $ret->function = $prev[1]->value; } } elseif ($token->value === ')' && $brackets == 0) { // Not our bracket break; } elseif ($token->value === ')') { --$brackets; if ($brackets === 0) { if (!empty($options['parenthesesDelimited'])) { // The current token is the last bracket, the next // one will be outside the expression. $ret->expr .= $token->token; ++$list->idx; break; } } elseif ($brackets < 0) { // $parser->error(__('Unexpected closing bracket.'), $token); // $brackets = 0; break; } } elseif ($token->value === ',') { // Expressions are comma-delimited. if ($brackets === 0) { break; } } } // Saving the previous tokens. $prev[0] = $prev[1]; $prev[1] = $token; if ($alias) { // An alias is expected (the keyword `AS` was previously found). if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); break; } $ret->alias = $token->value; $alias = false; } elseif ($isExpr) { // Handling aliases. if ($brackets === 0 && ($prev[0] === null || ($prev[0]->type !== Token::TYPE_OPERATOR || $prev[0]->token === ')') && ($prev[0]->type !== Token::TYPE_KEYWORD || !($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED))) && ($prev[1]->type === Token::TYPE_STRING || $prev[1]->type === Token::TYPE_SYMBOL && !($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE) || $prev[1]->type === Token::TYPE_NONE)) { if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); break; } $ret->alias = $prev[1]->value; } else { $ret->expr .= $token->token; } } elseif (!$isExpr) { if ($token->type === Token::TYPE_OPERATOR && $token->value === '.') { // Found a `.` which means we expect a column name and // the column name we parsed is actually the table name // and the table name is actually a database name. if (!empty($ret->database) || $dot) { $parser->error(__('Unexpected dot.'), $token); } $ret->database = $ret->table; $ret->table = $ret->column; $ret->column = null; $dot = true; $ret->expr .= $token->token; } else { $field = empty($options['field']) ? 'column' : $options['field']; if (empty($ret->{$field})) { $ret->{$field} = $token->value; $ret->expr .= $token->token; $dot = false; } else { // No alias is expected. if (!empty($options['breakOnAlias'])) { break; } if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); break; } $ret->alias = $token->value; } } } } if ($alias) { $parser->error(__('An alias was expected.'), $list->tokens[$list->idx - 1]); } // White-spaces might be added at the end. $ret->expr = trim($ret->expr); if ($ret->expr === '') { return null; } --$list->idx; return $ret; }
public function testParseBuild6() { $caseExprQuery = 'case when 1=1 then "Some" ' . 'when 1=2 then "SomeOther" end'; $component = CaseExpression::parse(new Parser(), $this->getTokensList($caseExprQuery)); $this->assertEquals(CaseExpression::build($component), 'CASE WHEN 1=1 THEN "Some" WHEN 1=2 THEN "SomeOther" END'); }