00001 <?php
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00015 abstract class OqlParser
00016 {
00017 const INITIAL_STATE = 254;
00018 const FINAL_STATE = 255;
00019
00020
00021 const PREFIX_UNARY_EXPRESSION = 1;
00022 const POSTFIX_UNARY_EXPRESSION = 2;
00023 const BINARY_EXPRESSION = 3;
00024 const BETWEEN_EXPRESSION = 4;
00025
00026 private static $classMap = array(
00027 self::PREFIX_UNARY_EXPRESSION => 'PrefixUnaryExpression',
00028 self::POSTFIX_UNARY_EXPRESSION => 'PostfixUnaryExpression',
00029 self::BINARY_EXPRESSION => 'BinaryExpression',
00030 self::BETWEEN_EXPRESSION => 'LogicalBetween'
00031 );
00032
00033
00034 private static $binaryOperatorMap = array(
00035 '=' => BinaryExpression::EQUALS,
00036 '!=' => BinaryExpression::NOT_EQUALS,
00037 'and' => BinaryExpression::EXPRESSION_AND,
00038 'or' => BinaryExpression::EXPRESSION_OR,
00039 '>' => BinaryExpression::GREATER_THAN,
00040 '>=' => BinaryExpression::GREATER_OR_EQUALS,
00041 '<' => BinaryExpression::LOWER_THAN,
00042 '<=' => BinaryExpression::LOWER_OR_EQUALS,
00043 'like' => BinaryExpression::LIKE,
00044 'not like' => BinaryExpression::NOT_LIKE,
00045 'ilike' => BinaryExpression::ILIKE,
00046 'not ilike' => BinaryExpression::NOT_ILIKE,
00047 'similar to' => BinaryExpression::SIMILAR_TO,
00048 'not similar to' => BinaryExpression::NOT_SIMILAR_TO,
00049 '+' => BinaryExpression::ADD,
00050 '-' => BinaryExpression::SUBSTRACT,
00051 '*' => BinaryExpression::MULTIPLY,
00052 '/' => BinaryExpression::DIVIDE
00053 );
00054
00055
00056 const LOGIC_PRIORITY_OR = 1;
00057 const LOGIC_PRIORITY_AND = 2;
00058 const LOGIC_PRIORITY_LT_GT = 3;
00059 const LOGIC_PRIORITY_EQ = 4;
00060 const LOGIC_PRIORITY_TERMINAL = 5;
00061
00062 const LOGIC_PRIORITY_LOWEST = self::LOGIC_PRIORITY_OR;
00063 const LOGIC_PRIORITY_UNARY_NOT = self::LOGIC_PRIORITY_LT_GT;
00064
00065 private static $logicPriorityMap = array(
00066 self::LOGIC_PRIORITY_OR => 'or',
00067 self::LOGIC_PRIORITY_AND => 'and',
00068 self::LOGIC_PRIORITY_LT_GT => array('>', '<', '>=', '<='),
00069 self::LOGIC_PRIORITY_EQ => array('=', '!='),
00070 self::LOGIC_PRIORITY_TERMINAL => null
00071 );
00072
00073
00074 const ARITHMETIC_PRIORITY_ADD = 1;
00075 const ARITHMETIC_PRIORITY_MUL = 2;
00076 const ARITHMETIC_PRIORITY_TERMINAL = 3;
00077
00078 const ARITHMETIC_PRIORITY_LOWEST = self::ARITHMETIC_PRIORITY_ADD;
00079
00080 private static $arithmeticPriorityMap = array(
00081 self::ARITHMETIC_PRIORITY_ADD => array('+', '-'),
00082 self::ARITHMETIC_PRIORITY_MUL => array('*', '/'),
00083 self::ARITHMETIC_PRIORITY_TERMINAL => null
00084 );
00085
00086 protected $state = null;
00087 protected $tokenizer = null;
00088 protected $oqlObject = null;
00089
00090 protected $parentheses = null;
00091
00095 abstract protected function makeOqlObject();
00096
00097 abstract protected function handleState();
00098
00102 public function parse($string = null)
00103 {
00104 if ($string === null) {
00105 Assert::isNotNull($this->tokenizer);
00106
00107 } else {
00108 Assert::isString($string);
00109 $this->tokenizer = new OqlTokenizer($string);
00110 }
00111
00112 $this->state = self::INITIAL_STATE;
00113 $this->oqlObject = $this->makeOqlObject();
00114 $this->parentheses = 0;
00115
00116 while ($this->state != self::FINAL_STATE)
00117 $this->state = $this->handleState();
00118
00119 $this->checkParentheses();
00120
00121 return $this->oqlObject;
00122 }
00123
00127 public function getTokenizer()
00128 {
00129 return $this->tokenizer;
00130 }
00131
00135 public function setTokenizer(OqlTokenizer $tokenizer)
00136 {
00137 $this->tokenizer = $tokenizer;
00138
00139 return $this;
00140 }
00141
00142 protected function getTokenValue($token, $raw = false)
00143 {
00144 if ($token instanceof OqlToken)
00145 return $raw
00146 ? $token->getRawValue()
00147 : $token->getValue();
00148
00149 return null;
00150 }
00151
00152 protected function checkToken($token, $type, $value = null)
00153 {
00154 if (
00155 $token instanceof OqlToken
00156 && $token->getType() == $type
00157 ) {
00158 if ($value === null) {
00159 return true;
00160
00161 } elseif (is_array($value)) {
00162 return in_array($token->getValue(), $value);
00163
00164 } else {
00165 return $token->getValue() == $value;
00166 }
00167 }
00168
00169 return false;
00170 }
00171
00172 protected function checkKeyword($token, $value)
00173 {
00174 return $this->checkToken($token, OqlToken::KEYWORD, $value);
00175 }
00176
00177 protected function checkIdentifier($token)
00178 {
00179 if ($token instanceof OqlToken) {
00180 if ($token->getType() == OqlToken::IDENTIFIER)
00181 return true;
00182
00183
00184
00185 elseif (
00186 $token->getType() == OqlToken::KEYWORD
00187 || $token->getType() == OqlToken::AGGREGATE_FUNCTION
00188 ) {
00189 $token->setValue($token->getRawValue());
00190
00191 return true;
00192 }
00193 }
00194
00195 return false;
00196 }
00197
00198 protected function checkConstant($token)
00199 {
00200 return
00201 $token instanceof OqlToken
00202 && (
00203 $token->getType() == OqlToken::STRING
00204 || $token->getType() == OqlToken::NUMBER
00205 || $token->getType() == OqlToken::BOOLEAN
00206 || $token->getType() == OqlToken::NULL
00207 || $token->getType() == OqlToken::SUBSTITUTION
00208 );
00209 }
00210
00211 protected function checkUnaryMinus($token)
00212 {
00213 return $this->checkToken($token, OqlToken::ARITHMETIC_OPERATOR, '-');
00214 }
00215
00219 protected function checkParentheses($message = null)
00220 {
00221 if ($this->openParentheses(false, $message)) {
00222 $this->error("unexpected '('", $message);
00223
00224 } elseif ($this->closeParentheses(false, $message)) {
00225 $this->error("unexpected ')'", $message);
00226 }
00227
00228 if ($this->parentheses > 0)
00229 $this->error("unexpected '('", $message);
00230
00231 return true;
00232 }
00233
00237 protected function openParentheses($required, $message = null)
00238 {
00239 if (
00240 $this->checkToken($this->tokenizer->peek(), OqlToken::PARENTHESES, '(')
00241 ) {
00242 $this->tokenizer->next();
00243 $this->parentheses++;
00244
00245 return true;
00246
00247 } elseif ($required) {
00248 $this->error("expecting ')'", $message);
00249 }
00250
00251 return false;
00252 }
00253
00257 protected function closeParentheses($required, $message = null)
00258 {
00259 if (
00260 $this->checkToken($this->tokenizer->peek(), OqlToken::PARENTHESES, ')')
00261 ) {
00262 $this->tokenizer->next();
00263 $this->parentheses--;
00264 if ($this->parentheses < 0)
00265 $this->error("unexpected ')'", $message);
00266
00267 return true;
00268
00269 } elseif ($required) {
00270 $this->error("expecting ')'", $message);
00271 }
00272
00273 return false;
00274 }
00275
00279 protected function getIdentifierExpression()
00280 {
00281 if ($isUnaryMinus = $this->checkUnaryMinus($this->tokenizer->peek()))
00282 $this->tokenizer->next();
00283
00284 $token = $this->tokenizer->peek();
00285
00286 if ($this->checkIdentifier($token)) {
00287 $this->tokenizer->next();
00288
00289 return $this->makeQuerySignedExpression($token, $isUnaryMinus);
00290 }
00291
00292 return null;
00293 }
00294
00298 protected function getConstantExpression()
00299 {
00300 if ($isUnaryMinus = $this->checkUnaryMinus($this->tokenizer->peek()))
00301 $this->tokenizer->next();
00302
00303 $token = $this->tokenizer->peek();
00304
00305 if (
00306 $token instanceof OqlToken
00307 && (
00308 (
00309 !$isUnaryMinus
00310 && (
00311 $token->getType() == OqlToken::STRING
00312 || $token->getType() == OqlToken::BOOLEAN
00313 || $token->getType() == OqlToken::NULL
00314 )
00315 ) || (
00316 $token->getType() == OqlToken::NUMBER
00317 || $token->getType() == OqlToken::SUBSTITUTION
00318 )
00319 )
00320 ) {
00321 $this->tokenizer->next();
00322
00323 return $this->makeQuerySignedExpression($token, $isUnaryMinus);
00324 }
00325
00326 return null;
00327 }
00328
00332 protected function getLogicExpression(
00333 $priority = self::LOGIC_PRIORITY_LOWEST
00334 )
00335 {
00336 $expression = null;
00337
00338
00339 if ($priority == self::LOGIC_PRIORITY_TERMINAL) {
00340 $token = $this->tokenizer->peek();
00341 if (!$token)
00342 return null;
00343
00344
00345 if ($this->isArithmeticExpression())
00346 return $this->getArithmeticExpression();
00347
00348
00349 if ($this->openParentheses(false)) {
00350 $expression = $this->getLogicExpression();
00351 $this->closeParentheses(true, 'in expression');
00352
00353 return $expression;
00354 }
00355
00356
00357 if ($this->checkKeyword($token, 'not')) {
00358 $this->tokenizer->next();
00359
00360 if (
00361 $argument = $this->getLogicExpression(self::LOGIC_PRIORITY_UNARY_NOT)
00362 ) {
00363 return $this->makeQueryExpression(
00364 self::$classMap[self::PREFIX_UNARY_EXPRESSION],
00365 PrefixUnaryExpression::NOT,
00366 $argument
00367 );
00368
00369 } else {
00370 $this->error('expecting argument in expression: not');
00371 }
00372 }
00373
00374
00375 if (
00376 !($expression = $this->getIdentifierExpression())
00377 && !($expression = $this->getConstantExpression())
00378 ) {
00379 $this->error(
00380 'expecting first argument in expression:',
00381 $this->getTokenValue($this->tokenizer->peek(), true)
00382 );
00383 }
00384
00385
00386 $operator = $this->tokenizer->peek();
00387 if ($this->checkKeyword($operator, 'not')) {
00388 $this->tokenizer->next();
00389 $operator = $this->tokenizer->peek();
00390 $isNot = true;
00391
00392 } else {
00393 $isNot = false;
00394 }
00395
00396
00397 if (
00398 !$isNot
00399 && $this->checkKeyword($operator, 'is')
00400 ) {
00401 $this->tokenizer->next();
00402
00403 $logic = null;
00404
00405 if ($this->checkKeyword($this->tokenizer->peek(), 'not')) {
00406 $this->tokenizer->next();
00407 $isNot = true;
00408
00409 } else {
00410 $isNot = false;
00411 }
00412
00413 if ($this->checkToken($this->tokenizer->peek(), OqlToken::NULL)) {
00414 $this->tokenizer->next();
00415 $logic = $isNot
00416 ? PostfixUnaryExpression::IS_NOT_NULL
00417 : PostfixUnaryExpression::IS_NULL;
00418
00419 } elseif (
00420 !$isNot
00421 && $this->checkToken($this->tokenizer->peek(), OqlToken::BOOLEAN)
00422 ) {
00423 $logic = $this->tokenizer->next()->getValue() === true
00424 ? PostfixUnaryExpression::IS_TRUE
00425 : PostfixUnaryExpression::IS_FALSE;
00426 }
00427
00428 if ($logic) {
00429 return $this->makeQueryExpression(
00430 self::$classMap[self::POSTFIX_UNARY_EXPRESSION],
00431 $expression,
00432 $logic
00433 );
00434
00435 } else {
00436 $this->error("expecting 'null', 'not null', 'true' or 'false'");
00437 }
00438
00439
00440 } elseif ($this->checkKeyword($operator, 'in')) {
00441 $isNotString = ($isNot ? 'not ' : '');
00442 $this->tokenizer->next();
00443
00444 $this->openParentheses(true, 'in expression: '.$isNotString.'in');
00445
00446 $list = $this->getCommaSeparatedList(
00447 array($this, 'getConstantExpression'),
00448 'expecting constant or substitution in expression: '
00449 .$isNotString.'in'
00450 );
00451
00452 if (is_array($list) && count($list) == 1)
00453 $list = reset($list);
00454
00455 $this->closeParentheses(true, 'in expression: '.$isNotString.'in');
00456
00457 return new OqlInExpression(
00458 $expression,
00459 $this->makeQueryParameter($list),
00460 $isNot ? InExpression::NOT_IN : InExpression::IN
00461 );
00462
00463
00464 } elseif (
00465 $this->checkKeyword($operator, array('like', 'ilike', 'similar to'))
00466 ) {
00467 $this->tokenizer->next();
00468
00469 $isNotString = ($isNot ? 'not ' : '');
00470 $argument = $this->tokenizer->next();
00471
00472 if (
00473 $this->checkToken($argument, OqlToken::STRING)
00474 || $this->checkToken($argument, OqlToken::SUBSTITUTION)
00475 ) {
00476 return $this->makeQueryExpression(
00477 self::$classMap[self::BINARY_EXPRESSION],
00478 $expression,
00479 $argument,
00480 self::$binaryOperatorMap[
00481 $isNotString
00482 .$this->getTokenValue($operator)
00483 ]
00484 );
00485
00486 } else {
00487 $this->error(
00488 'expecting string constant or substitution:',
00489 $isNotString.$this->getTokenValue($operator, true)
00490 );
00491 }
00492
00493
00494 } elseif (
00495 !$isNot
00496 && $this->checkKeyword($operator, 'between')
00497 ) {
00498 $this->tokenizer->next();
00499
00500 if (
00501 ($argument1 = $this->getIdentifierExpression())
00502 || ($argument1 = $this->getConstantExpression())
00503 ) {
00504 if ($this->checkKeyword($this->tokenizer->next(), 'and')) {
00505 if (
00506 ($argument2 = $this->getIdentifierExpression())
00507 || ($argument2 = $this->getConstantExpression())
00508 ) {
00509 return $this->makeQueryExpression(
00510 self::$classMap[self::BETWEEN_EXPRESSION],
00511 $expression,
00512 $argument1,
00513 $argument2
00514 );
00515
00516 } else {
00517 $this->error(
00518 'expecting second argument in expression: between'
00519 );
00520 }
00521
00522 } else {
00523 $this->error(
00524 "expecting 'and' in expression: between"
00525 );
00526 }
00527
00528 } else {
00529 $this->error(
00530 'expecting first argument in expression: between'
00531 );
00532 }
00533 }
00534
00535 if ($isNot)
00536 $this->error('expecting in, like, ilike or similar to');
00537
00538
00539 } else {
00540 $operatorList = self::$logicPriorityMap[$priority];
00541 $higherPriority = $priority + 1;
00542
00543 if (!($expression = $this->getLogicExpression($higherPriority))) {
00544 $this->error(
00545 'expecting first argument in expression:',
00546 is_array($operatorList)
00547 ? implode('|', $operatorList)
00548 : $operatorList
00549 );
00550 }
00551
00552 $tokenType =
00553 $priority == self::LOGIC_PRIORITY_OR
00554 || $priority == self::LOGIC_PRIORITY_AND
00555 ? OqlToken::KEYWORD
00556 : OqlToken::COMPARISON_OPERATOR;
00557
00558 while (
00559 $this->checkToken(
00560 $this->tokenizer->peek(),
00561 $tokenType,
00562 $operatorList
00563 )
00564 ) {
00565 $operator = $this->tokenizer->next();
00566
00567 if ($expression2 = $this->getLogicExpression($higherPriority)) {
00568 $expression = $this->makeQueryExpression(
00569 self::$classMap[self::BINARY_EXPRESSION],
00570 $expression,
00571 $expression2,
00572 self::$binaryOperatorMap[$operator->getValue()]
00573 );
00574
00575 } else {
00576 $this->error(
00577 'expecting second argument in expression:',
00578 $this->getTokenValue($operator, true)
00579 );
00580 }
00581 }
00582 }
00583
00584 return $expression;
00585 }
00586
00590 protected function getArithmeticExpression(
00591 $priority = self::ARITHMETIC_PRIORITY_LOWEST
00592 )
00593 {
00594
00595 if ($priority == self::ARITHMETIC_PRIORITY_TERMINAL) {
00596 $token = $this->tokenizer->peek();
00597 if (!$token)
00598 return null;
00599
00600
00601 if ($isUnaryMinus = $this->checkUnaryMinus($token))
00602 $this->tokenizer->next();
00603
00604
00605 if ($this->openParentheses(false)) {
00606 $expression = $this->getArithmeticExpression();
00607 $this->closeParentheses(true, 'in expression');
00608
00609
00610 } elseif ($expression = $this->getArithmeticArgumentExpression()) {
00611
00612
00613 } else {
00614 $this->error(
00615 'expecting argument in expression:',
00616 $this->getTokenValue($this->tokenizer->peek(), true)
00617 );
00618 }
00619
00620 $expression = $this->makeQuerySignedExpression($expression, $isUnaryMinus);
00621
00622
00623 } else {
00624 $operatorList = self::$arithmeticPriorityMap[$priority];
00625 $higherPriority = $priority + 1;
00626
00627 if (!($expression = $this->getArithmeticExpression($higherPriority))) {
00628 $this->error(
00629 'expecting first argument in expression:',
00630 implode('|', $operatorList)
00631 );
00632 }
00633
00634 while (
00635 $this->checkToken(
00636 $this->tokenizer->peek(),
00637 OqlToken::ARITHMETIC_OPERATOR,
00638 $operatorList
00639 )
00640 ) {
00641 $operator = $this->tokenizer->next();
00642
00643 if ($expression2 = $this->getArithmeticExpression($higherPriority)) {
00644 $expression = $this->makeQueryExpression(
00645 self::$classMap[self::BINARY_EXPRESSION],
00646 $expression,
00647 $expression2,
00648 self::$binaryOperatorMap[$operator->getValue()]
00649 );
00650
00651 } else {
00652 $this->error(
00653 'expecting second argument in expression:',
00654 $this->getTokenValue($operator, true)
00655 );
00656 }
00657 }
00658 }
00659
00660 return $expression;
00661 }
00662
00663 protected function getCommaSeparatedList($callback, $message)
00664 {
00665 $isComma = false;
00666 $list = array();
00667
00668 do {
00669 if ($isComma)
00670 $this->tokenizer->next();
00671
00672 if ($argument = call_user_func($callback))
00673 $list[] = $argument;
00674 else
00675 $this->error($message);
00676
00677 } while (
00678 $isComma
00679 = $this->checkToken($this->tokenizer->peek(), OqlToken::PUNCTUATION, ',')
00680 );
00681
00682 return $list;
00683 }
00684
00688 protected function makeQueryExpression($className )
00689 {
00690 $expression = OqlQueryExpression::create()->
00691 setClassName($className);
00692
00693 $arguments = func_get_args();
00694 reset($arguments);
00695 $argument = next($arguments);
00696
00697 while ($argument) {
00698 $expression->addParameter(
00699 $this->makeQueryParameter($argument)
00700 );
00701
00702 $argument = next($arguments);
00703 }
00704
00705 return $expression;
00706 }
00707
00711 protected function makeQuerySignedExpression($argument, $isUnaryMinus)
00712 {
00713 $expression = $this->makeQueryParameter($argument);
00714 if ($isUnaryMinus)
00715 $expression = new OqlPrefixMinusExpression($expression);
00716
00717 return $expression;
00718 }
00719
00723 protected function makeQueryParameter($argument)
00724 {
00725 if ($argument instanceof OqlQueryParameter) {
00726 return $argument;
00727
00728 } elseif ($argument instanceof OqlToken) {
00729 return OqlQueryParameter::create()->
00730 setValue($argument->getValue())->
00731 setBindable($argument->getType() == OqlToken::SUBSTITUTION);
00732
00733 } else {
00734 return OqlQueryParameter::create()->
00735 setValue($argument);
00736 }
00737 }
00738
00742 protected function error($message, $extraMessage = null)
00743 {
00744 if ($extraMessage)
00745 $message .= ' '.$extraMessage;
00746
00747 throw new SyntaxErrorException(
00748 $message,
00749 $this->tokenizer->getLine(),
00750 $this->tokenizer->getPosition()
00751 );
00752 }
00753
00754 private function isArithmeticExpression()
00755 {
00756 $index = $this->tokenizer->getIndex();
00757
00758
00759 while (
00760 $this->checkToken($this->tokenizer->peek(), OqlToken::PARENTHESES, '(')
00761 ) {
00762 $this->tokenizer->next();
00763 }
00764
00765
00766 if ($this->checkUnaryMinus($this->tokenizer->peek()))
00767 $this->tokenizer->next();
00768
00769 $result =
00770 $this->getArithmeticArgumentExpression()
00771 && $this->checkToken($this->tokenizer->peek(), OqlToken::ARITHMETIC_OPERATOR);
00772
00773 $this->tokenizer->setIndex($index);
00774
00775 return $result;
00776 }
00777
00781 private function getArithmeticArgumentExpression()
00782 {
00783 $token = $this->tokenizer->peek();
00784
00785 if (
00786 $this->checkIdentifier($token)
00787 || $this->checkToken($token, OqlToken::NUMBER)
00788 || $this->checkToken($token, OqlToken::SUBSTITUTION)
00789 ) {
00790 $this->tokenizer->next();
00791
00792 return $this->makeQueryParameter($token);
00793 }
00794
00795 return null;
00796 }
00797 }
00798 ?>