Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/** $Id$** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.** This software consists of voluntary contributions made by many individuals* and is licensed under the LGPL. For more information, see* <http://www.doctrine-project.org>.*//*** Doctrine_Query_Tokenizer** @package Doctrine* @subpackage Query* @license http://www.opensource.org/licenses/lgpl-license.php LGPL* @link www.doctrine-project.org* @since 1.0* @version $Revision$* @author Konsta Vesterinen <kvesteri@cc.hut.fi>* @author Guilherme Blanco <guilhermeblanco@hotmail.com>* @author Stefan Klug <stefan.klug@googlemail.com>*/class Doctrine_Query_Tokenizer{/*** Splits the given dql query into an array where keys represent different* query part names and values are arrays splitted using sqlExplode method** example:** parameter:* $query = "SELECT u.* FROM User u WHERE u.name LIKE ?"* returns:* array(* 'select' => array('u.*'),* 'from' => array('User', 'u'),* 'where' => array('u.name', 'LIKE', '?')* );** @param string $query DQL query** @throws Doctrine_Query_Exception If some generic parsing error occurs** @return array An array containing the query string parts*/public function tokenizeQuery($query){$tokens = $this->sqlExplode($query, ' ');$parts = array();foreach ($tokens as $index => $token) {$token = trim($token);switch (strtolower($token)) {case 'delete':case 'update':case 'select':case 'set':case 'from':case 'where':case 'limit':case 'offset':case 'having':$p = $token;//$parts[$token] = array();$parts[$token] = '';break;case 'order':case 'group':$i = ($index + 1);if (isset($tokens[$i]) && strtolower($tokens[$i]) === 'by') {$p = $token;$parts[$token] = '';//$parts[$token] = array();} else {$parts[$p] .= "$token ";//$parts[$p][] = $token;}break;case 'by':continue;default:if ( ! isset($p)) {throw new Doctrine_Query_Tokenizer_Exception("Couldn't tokenize query. Encountered invalid token: '$token'.");}$parts[$p] .= "$token ";//$parts[$p][] = $token;}}return $parts;}/*** Trims brackets from string** @param string $str String to remove the brackets* @param string $e1 First bracket, usually '('* @param string $e2 Second bracket, usually ')'** @return string*/public function bracketTrim($str, $e1 = '(', $e2 = ')'){if (substr($str, 0, 1) === $e1 && substr($str, -1) === $e2) {return substr($str, 1, -1);} else {return $str;}}/*** Explodes a sql expression respecting bracket placement.** This method transform a sql expression in an array of simple clauses,* while observing the parentheses precedence.** Note: bracketExplode always trims the returned pieces** <code>* $str = (age < 20 AND age > 18) AND email LIKE 'John@example.com'* $clauses = $tokenizer->bracketExplode($str, ' AND ', '(', ')');* // array("(age < 20 AND age > 18)", "email LIKE 'John@example.com'")* </code>** @param string $str String to be bracket exploded* @param string $d Delimeter which explodes the string* @param string $e1 First bracket, usually '('* @param string $e2 Second bracket, usually ')'** @return array*/public function bracketExplode($str, $d = ' ', $e1 = '(', $e2 = ')'){if (is_string($d)) {$d = array($d);}// Bracket explode has to be case insensitive$regexp = $this->getSplitRegExpFromArray($d) . 'i';$terms = $this->clauseExplodeRegExp($str, $regexp, $e1, $e2);$res = array();// Trim is here for historical reasonsforeach ($terms as $value) {$res[] = trim($value[0]);}return $res;}/*** Explode quotes from string** Note: quoteExplode always trims the returned pieces** example:** parameters:* $str = email LIKE 'John@example.com'* $d = ' LIKE '** would return an array:* array("email", "LIKE", "'John@example.com'")** @param string $str String to be quote exploded* @param string $d Delimeter which explodes the string** @return array*/public function quoteExplode($str, $d = ' '){if (is_string($d)) {$d = array($d);}// According to the testcases quoteExplode is case insensitive$regexp = $this->getSplitRegExpFromArray($d) . 'i';$terms = $this->clauseExplodeCountBrackets($str, $regexp);$res = array();foreach ($terms as $val) {$res[] = trim($val[0]);}return $res;}/*** Explodes a string into array using custom brackets and* quote delimeters** Note: sqlExplode trims all returned parts** example:** parameters:* $str = "(age < 20 AND age > 18) AND name LIKE 'John Doe'"* $d = ' '* $e1 = '('* $e2 = ')'** would return an array:* array(* '(age < 20 AND age > 18)',* 'name',* 'LIKE',* 'John Doe'* );** @param string $str String to be SQL exploded* @param string $d Delimeter which explodes the string* @param string $e1 First bracket, usually '('* @param string $e2 Second bracket, usually ')'** @return array*/public function sqlExplode($str, $d = ' ', $e1 = '(', $e2 = ')'){if (is_string($d)) {$d = array($d);}$terms = $this->clauseExplode($str, $d, $e1, $e2);$res = array();foreach ($terms as $value) {$res[] = trim($value[0]);}return $res;}/*** Explodes a string into array using custom brackets and quote delimeters* Each array element is a array of length 2 where the first entry contains* the term, and the second entry contains the corresponding delimiter** example:** parameters:* $str = "(age < 20 AND age > 18) AND name LIKE 'John'+' Doe'"* $d = array(' ', '+')* $e1 = '('* $e2 = ')'** would return an array:* array(* array('(age < 20 AND age > 18)', ' '),* array('AND', ' '),* array('name', ' '),* array('LIKE', ' '),* array('John', '+'),* array(' Doe', '')* );** @param string $str String to be clause exploded* @param string $d Delimeter which explodes the string* @param string $e1 First bracket, usually '('* @param string $e2 Second bracket, usually ')'** @return array*/public function clauseExplode($str, array $d, $e1 = '(', $e2 = ')'){$regexp = $this->getSplitRegExpFromArray($d);return $this->clauseExplodeRegExp($str, $regexp, $e1, $e2);}/*** Builds regular expression for split from array. Return regular* expression to be applied** @param $d** @return string*/private function getSplitRegExpFromArray(array $d){foreach ($d as $key => $string) {$escapedString = preg_quote($string);if (preg_match('#^\w+$#', $string)) {$escapedString = "\W$escapedString\W";}$d[$key] = $escapedString;}if (in_array(' ', $d)) {$d[] = '\s';}return '#(' . implode('|', $d) . ')#';}/*** Same as clauseExplode, but you give a regexp, which splits the string** @param $str* @param $regexp* @param $e1* @param $e2** @return array*/private function clauseExplodeRegExp($str, $regexp, $e1 = '(', $e2 = ')'){$terms = $this->clauseExplodeCountBrackets($str, $regexp, $e1, $e2);$terms = $this->mergeBracketTerms($terms);// This is only here to comply with the old function signatureforeach ($terms as & $val) {unset($val[2]);}return $terms;}/*** this function is like clauseExplode, but it doesn't merge bracket terms** @param $str* @param $d* @param $e1* @param $e2** @return unknown_type*/private function clauseExplodeCountBrackets($str, $regexp, $e1 = '(', $e2 = ')'){$quoteTerms = $this->quotedStringExplode($str);$terms = array();$i = 0;foreach ($quoteTerms as $key => $val) {if ($key & 1) { // a quoted string// If the last term had no ending delimiter, we append the string to the element,// otherwise, we create a new element without delimiterif ($terms[$i - 1][1] == '') {$terms[$i - 1][0] .= $val;} else {$terms[$i++] = array($val, '', 0);}} else { // Not a quoted string// Do the clause explode$subterms = $this->clauseExplodeNonQuoted($val, $regexp);foreach ($subterms as &$sub) {$c1 = substr_count($sub[0], $e1);$c2 = substr_count($sub[0], $e2);$sub[2] = $c1 - $c2;}// If the previous term had no delimiter, merge themif ($i > 0 && $terms[$i - 1][1] == '') {$first = array_shift($subterms);$idx = $i - 1;$terms[$idx][0] .= $first[0];$terms[$idx][1] = $first[1];$terms[$idx][2] += $first[2];}$terms = array_merge($terms, $subterms);$i += sizeof($subterms);}}return $terms;}/*** Explodes a string by the given delimiters, and counts quotes in every* term. This function doesn't respect quoted strings.* The returned array contains a array per term. These term array contain* the following elemnts:* [0] = the term itself* [1] = the delimiter splitting this term from the next* [2] = the sum of opening and closing brackets in this term* (eg. -2 means 2 closing brackets (or 1 opening and 3 closing))** example:** parameters:* $str = "a (b '(c+d))'"* $d = array(' ', '+')** returns:* array(* array('a', ' ', 0),* array('(b', ' ', 1),* array("'(c", '+', 1),* array("d))'", '', -2)* );** @param $str* @param $d* @param $e1* @param $e2** @return array*/private function clauseExplodeNonQuoted($str, $regexp){$str = preg_split($regexp, $str, -1, PREG_SPLIT_DELIM_CAPTURE);$term = array();$i = 0;foreach ($str as $key => $val) {// Every odd entry is a delimiter, so add it to the previous term entryif ( ! ($key & 1)) {$term[$i] = array($val, '');} else {$term[$i++][1] = $val;}}return $term;}/*** This expects input from clauseExplodeNonQuoted.* It will go through the result and merges any bracket terms with* unbalanced bracket count.* Note that only the third parameter in each term is used to get the* bracket overhang. This is needed to be able to handle quoted strings* wich contain brackets** example:** parameters:* $terms = array(* array("'a(b'", '+', 0)* array('(2', '+', 1),* array('3)', '-', -1),* array('5', '' , '0')* );** would return:* array(* array("'a(b'", '+', 0),* array('(2+3)', '-', 0),* array('5' , '' , 0)* );** @param $terms array** @return array*/private function mergeBracketTerms(array $terms){$res = array();$i = 0;foreach ($terms as $val) {if ( ! isset($res[$i])) {$res[$i] = array($val[0], $val[1], $val[2]);} else {$res[$i][0] .= $res[$i][1] . $val[0];$res[$i][1] = $val[1];$res[$i][2] += $val[2];}// Bracket overhangif ($res[$i][2] == 0) {$i++;}}return $res;}/*** Explodes the given string by <quoted words>** example:** paramters:* $str ="'a' AND name = 'John O\'Connor'"** returns* array("", "'a'", " AND name = ", "'John O\'Connor'")** Note the trailing empty string. In the result, all even elements are quoted strings.** @param $str the string to split** @return array*/public function quotedStringExplode($str){// Split by all possible incarnations of a quote$split = array_map('preg_quote', array("\\'","''","'", "\\\"", "\"\"", "\""));$split = '#(' . implode('|', $split) . ')#';$str = preg_split($split, $str, -1, PREG_SPLIT_DELIM_CAPTURE);$parts = array();$mode = false; // Mode is either ' or " if the loop is inside a string quoted with ' or "$i = 0;foreach ($str as $key => $val) {// This is some kind of quoteif ($key & 1) {if ( ! $mode) {if ($val == "'" || $val == "\"") {$mode = $val;$i++;}} else if ($mode == $val) {if ( ! isset($parts[$i])) {$parts[$i] = $val;} else {$parts[$i] .= $val;}$mode = false;$i++;continue;}}if ( ! isset($parts[$i])) {$parts[$i] = $val;} else {$parts[$i] .= $val;}}return $parts;}}