. */ namespace Doctrine\ORM\Query; /** * Scans a DQL query for tokens. * * @author Guilherme Blanco * @author Janne Vanhala * @author Roman Borschel * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.phpdoctrine.org * @since 2.0 * @version $Revision$ */ class Lexer { const T_NONE = 1; const T_IDENTIFIER = 2; const T_INTEGER = 3; const T_STRING = 4; const T_INPUT_PARAMETER = 5; const T_FLOAT = 6; const T_ALL = 101; const T_AND = 102; const T_ANY = 103; const T_AS = 104; const T_ASC = 105; const T_AVG = 106; const T_BETWEEN = 107; const T_BY = 108; const T_COMMA = 109; const T_COUNT = 110; const T_DELETE = 111; const T_DESC = 112; const T_DISTINCT = 113; const T_DOT = 114; const T_ESCAPE = 115; const T_EXISTS = 116; const T_FROM = 117; const T_GROUP = 118; const T_HAVING = 119; const T_IN = 120; const T_INDEX = 121; const T_INNER = 122; const T_IS = 123; const T_JOIN = 124; const T_LEFT = 125; const T_LIKE = 126; const T_LIMIT = 127; const T_MAX = 128; const T_MIN = 129; const T_MOD = 130; const T_NOT = 131; const T_NULL = 132; const T_OFFSET = 133; const T_ON = 134; const T_OR = 135; const T_ORDER = 136; const T_OUTER = 137; const T_SELECT = 138; const T_SET = 139; const T_SIZE = 140; const T_SOME = 141; const T_SUM = 142; const T_UPDATE = 143; const T_WHERE = 144; const T_WITH = 145; const T_TRUE = 146; const T_FALSE = 147; private $_keywordsTable; /** * Array of scanned tokens. * * @var array */ private $_tokens = array(); /** * @todo Doc */ private $_position = 0; /** * @todo Doc */ private $_peek = 0; /** * @var array The next token in the query string. */ public $lookahead; /** * @var array The last matched token. */ public $token; /** * Creates a new query scanner object. * * @param string $input a query string */ public function __construct($input) { $this->_scan($input); } /** * Checks whether a given token matches the current lookahead. * * @param $token * @return */ public function isNextToken($token) { $la = $this->lookahead; return ($la['type'] === $token || $la['value'] === $token); } /** * Moves to the next token in the input string. * * A token is an associative array containing three items: * - 'value' : the string value of the token in the input string * - 'type' : the type of the token (identifier, numeric, string, input * parameter, none) * - 'position' : the position of the token in the input string * * @return array|null the next token; null if there is no more tokens left */ public function next() { $this->token = $this->lookahead; $this->_peek = 0; if (isset($this->_tokens[$this->_position])) { $this->lookahead = $this->_tokens[$this->_position++]; } else { $this->lookahead = null; } } /** * Returns the next token in the input string. * * A token is an associative array containing three items: * - 'value' : the string value of the token in the input string * - 'type' : the type of the token (identifier, numeric, string, input * parameter, none) * - 'position' : the position of the token in the input string * * @return array|null the next token; null if there is no more tokens left */ /*public function next() { $this->_peek = 0; if (isset($this->_tokens[$this->_position])) { return $this->_tokens[$this->_position++]; } else { return null; } }*/ /** * Checks if an identifier is a keyword and returns its correct type. * * @param string $identifier identifier name * @return int token type */ public function _checkLiteral($identifier) { $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($identifier); if (defined($name)) { $type = constant($name); if ($type > 100) { return $type; } } return self::T_IDENTIFIER; } /** * Scans the input string for tokens. * * @param string $input a query string */ protected function _scan($input) { static $regex; if ( ! isset($regex)) { $patterns = array( '[a-z_][a-z0-9_\\\]*', '(?:[0-9]+(?:[,\.][0-9]+)*)(?:e[+-]?[0-9]+)?', "'(?:[^']|'')*'", '\?[0-9]+|:[a-z][a-z0-9_]+' ); $regex = '/(' . implode(')|(', $patterns) . ')|\s+|(.)/i'; } $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; $matches = preg_split($regex, $input, -1, $flags); foreach ($matches as $match) { $value = $match[0]; $type = $this->_getType($value); $this->_tokens[] = array( 'value' => $value, 'type' => $type, 'position' => $match[1] ); } } /** * @todo Doc */ protected function _getType(&$value) { // $value is referenced because it can be changed if it is numeric. // [TODO] Revisit the _isNumeric and _getNumeric methods to reduce overhead. $type = self::T_NONE; $newVal = $this->_getNumeric($value); if ($newVal !== false){ $value = $newVal; if (strpos($value, '.') !== false || stripos($value, 'e') !== false) { $type = self::T_FLOAT; } else { $type = self::T_INTEGER; } } if ($value[0] === "'" && $value[strlen($value) - 1] === "'") { $type = self::T_STRING; } else if (ctype_alpha($value[0]) || $value[0] === '_') { $type = $this->_checkLiteral($value); } else if ($value[0] === '?' || $value[0] === ':') { $type = self::T_INPUT_PARAMETER; } return $type; } /** * @todo Doc */ protected function _getNumeric($value) { if ( ! is_scalar($value)) { return false; } // Checking for valid numeric numbers: 1.234, -1.234e-2 if (is_numeric($value)) { return $value; } // World number: 1.000.000,02 or -1,234e-2 $worldnum = strtr($value, array('.' => '', ',' => '.')); if (is_numeric($worldnum)) { return $worldnum; } // American extensive number: 1,000,000.02 $american_en = strtr($value, array(',' => '')); if (is_numeric($american_en)) { return $american_en; } return false; } /** * @todo Doc */ public function isA($value, $token) { $type = $this->_getType($value); return $type === $token; } /** * Moves the lookahead token forward. * * @return array|null The next token or NULL if there are no more tokens ahead. */ public function peek() { if (isset($this->_tokens[$this->_position + $this->_peek])) { return $this->_tokens[$this->_position + $this->_peek++]; } else { return null; } } /** * Peeks at the next token, returns it and immediately resets the peek. * * @return array|null The next token or NULL if there are no more tokens ahead. */ public function glimpse() { $peek = $this->peek(); $this->_peek = 0; return $peek; } /** * @todo Doc */ public function resetPeek() { $this->_peek = 0; } /** * @todo Doc */ public function resetPosition($position = 0) { $this->_position = $position; } private function _addKeyword($token, $value) { $this->_keywordsTable[$token] = $value; } public function getLiteral($token) { if ( ! $this->_keywordsTable) { $this->_addKeyword(self::T_ALL, "ALL"); $this->_addKeyword(self::T_AND, "AND"); $this->_addKeyword(self::T_ANY, "ANY"); $this->_addKeyword(self::T_AS, "AS"); $this->_addKeyword(self::T_ASC, "ASC"); $this->_addKeyword(self::T_AVG, "AVG"); $this->_addKeyword(self::T_BETWEEN, "BETWEEN"); $this->_addKeyword(self::T_BY, "BY"); $this->_addKeyword(self::T_COMMA, ","); $this->_addKeyword(self::T_COUNT, "COUNT"); $this->_addKeyword(self::T_DELETE, "DELETE"); $this->_addKeyword(self::T_DESC, "DESC"); $this->_addKeyword(self::T_DISTINCT, "DISTINCT"); $this->_addKeyword(self::T_DOT, "."); $this->_addKeyword(self::T_ESCAPE, "ESPACE"); $this->_addKeyword(self::T_EXISTS, "EXISTS"); $this->_addKeyword(self::T_FALSE, "FALSE"); $this->_addKeyword(self::T_FROM, "FROM"); $this->_addKeyword(self::T_GROUP, "GROUP"); $this->_addKeyword(self::T_HAVING, "HAVING"); $this->_addKeyword(self::T_IN, "IN"); $this->_addKeyword(self::T_INDEX, "INDEX"); $this->_addKeyword(self::T_INNER, "INNER"); $this->_addKeyword(self::T_IS, "IS"); $this->_addKeyword(self::T_JOIN, "JOIN"); $this->_addKeyword(self::T_LEFT, "LEFT"); $this->_addKeyword(self::T_LIKE, "LIKE"); $this->_addKeyword(self::T_LIMIT, "LIMIT"); $this->_addKeyword(self::T_MAX, "MAX"); $this->_addKeyword(self::T_MIN, "MIN"); $this->_addKeyword(self::T_MOD, "MOD"); $this->_addKeyword(self::T_NOT, "NOT"); $this->_addKeyword(self::T_NULL, "NULL"); $this->_addKeyword(self::T_OFFSET, "OFFSET"); $this->_addKeyword(self::T_ON, "ON"); $this->_addKeyword(self::T_OR, "OR"); $this->_addKeyword(self::T_ORDER, "ORDER"); $this->_addKeyword(self::T_OUTER, "OUTER"); $this->_addKeyword(self::T_SELECT, "SELECT"); $this->_addKeyword(self::T_SET, "SET"); $this->_addKeyword(self::T_SIZE, "SIZE"); $this->_addKeyword(self::T_SOME, "SOME"); $this->_addKeyword(self::T_SUM, "SUM"); $this->_addKeyword(self::T_TRUE, "TRUE"); $this->_addKeyword(self::T_UPDATE, "UPDATE"); $this->_addKeyword(self::T_WHERE, "WHERE"); $this->_addKeyword(self::T_WITH, "WITH"); } return isset($this->_keywordsTable[$token]) ? $this->_keywordsTable[$token] : (is_string($token) ? $token : ''); } }