vendor/doctrine/orm/src/Query/Parser.php line 263

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Query;
  4. use Doctrine\Common\Lexer\Token;
  5. use Doctrine\Deprecations\Deprecation;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use Doctrine\ORM\Mapping\ClassMetadata;
  8. use Doctrine\ORM\Query;
  9. use Doctrine\ORM\Query\AST\Functions;
  10. use Doctrine\ORM\Query\Exec\SqlFinalizer;
  11. use LogicException;
  12. use ReflectionClass;
  13. use function array_intersect;
  14. use function array_search;
  15. use function assert;
  16. use function class_exists;
  17. use function count;
  18. use function explode;
  19. use function implode;
  20. use function in_array;
  21. use function interface_exists;
  22. use function is_string;
  23. use function sprintf;
  24. use function str_contains;
  25. use function strlen;
  26. use function strpos;
  27. use function strrpos;
  28. use function strtolower;
  29. use function substr;
  30. /**
  31. * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
  32. * Parses a DQL query, reports any errors in it, and generates an AST.
  33. *
  34. * @phpstan-import-type AssociationMapping from ClassMetadata
  35. * @phpstan-type DqlToken = Token<TokenType::T_*, string>
  36. * @phpstan-type QueryComponent = array{
  37. * metadata?: ClassMetadata<object>,
  38. * parent?: string|null,
  39. * relation?: AssociationMapping|null,
  40. * map?: string|null,
  41. * resultVariable?: AST\Node|string,
  42. * nestingLevel: int,
  43. * token: DqlToken,
  44. * }
  45. */
  46. class Parser
  47. {
  48. /**
  49. * @readonly Maps BUILT-IN string function names to AST class names.
  50. * @var array<string, class-string<Functions\FunctionNode>>
  51. */
  52. private static $stringFunctions = [
  53. 'concat' => Functions\ConcatFunction::class,
  54. 'substring' => Functions\SubstringFunction::class,
  55. 'trim' => Functions\TrimFunction::class,
  56. 'lower' => Functions\LowerFunction::class,
  57. 'upper' => Functions\UpperFunction::class,
  58. 'identity' => Functions\IdentityFunction::class,
  59. ];
  60. /**
  61. * @readonly Maps BUILT-IN numeric function names to AST class names.
  62. * @var array<string, class-string<Functions\FunctionNode>>
  63. */
  64. private static $numericFunctions = [
  65. 'length' => Functions\LengthFunction::class,
  66. 'locate' => Functions\LocateFunction::class,
  67. 'abs' => Functions\AbsFunction::class,
  68. 'sqrt' => Functions\SqrtFunction::class,
  69. 'mod' => Functions\ModFunction::class,
  70. 'size' => Functions\SizeFunction::class,
  71. 'date_diff' => Functions\DateDiffFunction::class,
  72. 'bit_and' => Functions\BitAndFunction::class,
  73. 'bit_or' => Functions\BitOrFunction::class,
  74. // Aggregate functions
  75. 'min' => Functions\MinFunction::class,
  76. 'max' => Functions\MaxFunction::class,
  77. 'avg' => Functions\AvgFunction::class,
  78. 'sum' => Functions\SumFunction::class,
  79. 'count' => Functions\CountFunction::class,
  80. ];
  81. /**
  82. * @readonly Maps BUILT-IN datetime function names to AST class names.
  83. * @var array<string, class-string<Functions\FunctionNode>>
  84. */
  85. private static $datetimeFunctions = [
  86. 'current_date' => Functions\CurrentDateFunction::class,
  87. 'current_time' => Functions\CurrentTimeFunction::class,
  88. 'current_timestamp' => Functions\CurrentTimestampFunction::class,
  89. 'date_add' => Functions\DateAddFunction::class,
  90. 'date_sub' => Functions\DateSubFunction::class,
  91. ];
  92. /*
  93. * Expressions that were encountered during parsing of identifiers and expressions
  94. * and still need to be validated.
  95. */
  96. /** @phpstan-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
  97. private $deferredIdentificationVariables = [];
  98. /** @phpstan-var list<array{token: DqlToken|null, expression: AST\PartialObjectExpression, nestingLevel: int}> */
  99. private $deferredPartialObjectExpressions = [];
  100. /** @phpstan-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */
  101. private $deferredPathExpressions = [];
  102. /** @phpstan-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
  103. private $deferredResultVariables = [];
  104. /** @phpstan-var list<array{token: DqlToken|null, expression: AST\NewObjectExpression, nestingLevel: int}> */
  105. private $deferredNewObjectExpressions = [];
  106. /**
  107. * The lexer.
  108. *
  109. * @var Lexer
  110. */
  111. private $lexer;
  112. /**
  113. * The parser result.
  114. *
  115. * @var ParserResult
  116. */
  117. private $parserResult;
  118. /**
  119. * The EntityManager.
  120. *
  121. * @var EntityManagerInterface
  122. */
  123. private $em;
  124. /**
  125. * The Query to parse.
  126. *
  127. * @var Query
  128. */
  129. private $query;
  130. /**
  131. * Map of declared query components in the parsed query.
  132. *
  133. * @phpstan-var array<string, QueryComponent>
  134. */
  135. private $queryComponents = [];
  136. /**
  137. * Keeps the nesting level of defined ResultVariables.
  138. *
  139. * @var int
  140. */
  141. private $nestingLevel = 0;
  142. /** @var int */
  143. private $withJoinConditionNestingLevel = 0;
  144. /**
  145. * Any additional custom tree walkers that modify the AST.
  146. *
  147. * @var list<class-string<TreeWalker>>
  148. */
  149. private $customTreeWalkers = [];
  150. /**
  151. * The custom last tree walker, if any, that is responsible for producing the output.
  152. *
  153. * @var class-string<SqlWalker>|null
  154. */
  155. private $customOutputWalker;
  156. /** @phpstan-var array<string, AST\SelectExpression> */
  157. private $identVariableExpressions = [];
  158. /**
  159. * Creates a new query parser object.
  160. *
  161. * @param Query $query The Query to parse.
  162. */
  163. public function __construct(Query $query)
  164. {
  165. $this->query = $query;
  166. $this->em = $query->getEntityManager();
  167. $this->lexer = new Lexer((string) $query->getDQL());
  168. $this->parserResult = new ParserResult();
  169. }
  170. /**
  171. * Sets a custom tree walker that produces output.
  172. * This tree walker will be run last over the AST, after any other walkers.
  173. *
  174. * @param class-string<SqlWalker> $className
  175. *
  176. * @return void
  177. */
  178. public function setCustomOutputTreeWalker($className)
  179. {
  180. Deprecation::trigger(
  181. 'doctrine/orm',
  182. 'https://github.com/doctrine/orm/pull/11641',
  183. '%s is deprecated, set the output walker class with the \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER query hint instead',
  184. __METHOD__
  185. );
  186. $this->customOutputWalker = $className;
  187. }
  188. /**
  189. * Adds a custom tree walker for modifying the AST.
  190. *
  191. * @param class-string<TreeWalker> $className
  192. *
  193. * @return void
  194. */
  195. public function addCustomTreeWalker($className)
  196. {
  197. $this->customTreeWalkers[] = $className;
  198. }
  199. /**
  200. * Gets the lexer used by the parser.
  201. *
  202. * @return Lexer
  203. */
  204. public function getLexer()
  205. {
  206. return $this->lexer;
  207. }
  208. /**
  209. * Gets the ParserResult that is being filled with information during parsing.
  210. *
  211. * @return ParserResult
  212. */
  213. public function getParserResult()
  214. {
  215. return $this->parserResult;
  216. }
  217. /**
  218. * Gets the EntityManager used by the parser.
  219. *
  220. * @return EntityManagerInterface
  221. */
  222. public function getEntityManager()
  223. {
  224. return $this->em;
  225. }
  226. /**
  227. * Parses and builds AST for the given Query.
  228. *
  229. * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
  230. */
  231. public function getAST()
  232. {
  233. // Parse & build AST
  234. $AST = $this->QueryLanguage();
  235. // Process any deferred validations of some nodes in the AST.
  236. // This also allows post-processing of the AST for modification purposes.
  237. $this->processDeferredIdentificationVariables();
  238. if ($this->deferredPartialObjectExpressions) {
  239. $this->processDeferredPartialObjectExpressions();
  240. }
  241. if ($this->deferredPathExpressions) {
  242. $this->processDeferredPathExpressions();
  243. }
  244. if ($this->deferredResultVariables) {
  245. $this->processDeferredResultVariables();
  246. }
  247. if ($this->deferredNewObjectExpressions) {
  248. $this->processDeferredNewObjectExpressions($AST);
  249. }
  250. $this->processRootEntityAliasSelected();
  251. // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
  252. $this->fixIdentificationVariableOrder($AST);
  253. return $AST;
  254. }
  255. /**
  256. * Attempts to match the given token with the current lookahead token.
  257. *
  258. * If they match, updates the lookahead token; otherwise raises a syntax
  259. * error.
  260. *
  261. * @param TokenType::T_* $token The token type.
  262. *
  263. * @return void
  264. *
  265. * @throws QueryException If the tokens don't match.
  266. */
  267. public function match($token)
  268. {
  269. $lookaheadType = $this->lexer->lookahead->type ?? null;
  270. // Short-circuit on first condition, usually types match
  271. if ($lookaheadType === $token) {
  272. $this->lexer->moveNext();
  273. return;
  274. }
  275. // If parameter is not identifier (1-99) must be exact match
  276. if ($token < TokenType::T_IDENTIFIER) {
  277. $this->syntaxError($this->lexer->getLiteral($token));
  278. }
  279. // If parameter is keyword (200+) must be exact match
  280. if ($token > TokenType::T_IDENTIFIER) {
  281. $this->syntaxError($this->lexer->getLiteral($token));
  282. }
  283. // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
  284. if ($token === TokenType::T_IDENTIFIER && $lookaheadType < TokenType::T_IDENTIFIER) {
  285. $this->syntaxError($this->lexer->getLiteral($token));
  286. }
  287. $this->lexer->moveNext();
  288. }
  289. /**
  290. * Frees this parser, enabling it to be reused.
  291. *
  292. * @param bool $deep Whether to clean peek and reset errors.
  293. * @param int $position Position to reset.
  294. *
  295. * @return void
  296. */
  297. public function free($deep = false, $position = 0)
  298. {
  299. // WARNING! Use this method with care. It resets the scanner!
  300. $this->lexer->resetPosition($position);
  301. // Deep = true cleans peek and also any previously defined errors
  302. if ($deep) {
  303. $this->lexer->resetPeek();
  304. }
  305. $this->lexer->token = null;
  306. $this->lexer->lookahead = null;
  307. }
  308. /**
  309. * Parses a query string.
  310. *
  311. * @return ParserResult
  312. */
  313. public function parse()
  314. {
  315. $AST = $this->getAST();
  316. $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
  317. if ($customWalkers !== false) {
  318. $this->customTreeWalkers = $customWalkers;
  319. }
  320. $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
  321. if ($customOutputWalker !== false) {
  322. $this->customOutputWalker = $customOutputWalker;
  323. }
  324. // Run any custom tree walkers over the AST
  325. if ($this->customTreeWalkers) {
  326. $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
  327. foreach ($this->customTreeWalkers as $walker) {
  328. $treeWalkerChain->addTreeWalker($walker);
  329. }
  330. switch (true) {
  331. case $AST instanceof AST\UpdateStatement:
  332. $treeWalkerChain->walkUpdateStatement($AST);
  333. break;
  334. case $AST instanceof AST\DeleteStatement:
  335. $treeWalkerChain->walkDeleteStatement($AST);
  336. break;
  337. case $AST instanceof AST\SelectStatement:
  338. default:
  339. $treeWalkerChain->walkSelectStatement($AST);
  340. }
  341. $this->queryComponents = $treeWalkerChain->getQueryComponents();
  342. }
  343. $outputWalkerClass = $this->customOutputWalker ?: SqlOutputWalker::class;
  344. $outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
  345. if ($outputWalker instanceof OutputWalker) {
  346. $finalizer = $outputWalker->getFinalizer($AST);
  347. $this->parserResult->setSqlFinalizer($finalizer);
  348. } else {
  349. Deprecation::trigger(
  350. 'doctrine/orm',
  351. 'https://github.com/doctrine/orm/pull/11188/',
  352. 'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
  353. $outputWalkerClass,
  354. OutputWalker::class,
  355. SqlFinalizer::class
  356. );
  357. // @phpstan-ignore method.deprecated
  358. $executor = $outputWalker->getExecutor($AST);
  359. // @phpstan-ignore method.deprecated
  360. $this->parserResult->setSqlExecutor($executor);
  361. }
  362. return $this->parserResult;
  363. }
  364. /**
  365. * Fixes order of identification variables.
  366. *
  367. * They have to appear in the select clause in the same order as the
  368. * declarations (from ... x join ... y join ... z ...) appear in the query
  369. * as the hydration process relies on that order for proper operation.
  370. *
  371. * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
  372. */
  373. private function fixIdentificationVariableOrder(AST\Node $AST): void
  374. {
  375. if (count($this->identVariableExpressions) <= 1) {
  376. return;
  377. }
  378. assert($AST instanceof AST\SelectStatement);
  379. foreach ($this->queryComponents as $dqlAlias => $qComp) {
  380. if (! isset($this->identVariableExpressions[$dqlAlias])) {
  381. continue;
  382. }
  383. $expr = $this->identVariableExpressions[$dqlAlias];
  384. $key = array_search($expr, $AST->selectClause->selectExpressions, true);
  385. unset($AST->selectClause->selectExpressions[$key]);
  386. $AST->selectClause->selectExpressions[] = $expr;
  387. }
  388. }
  389. /**
  390. * Generates a new syntax error.
  391. *
  392. * @param string $expected Expected string.
  393. * @param mixed[]|null $token Got token.
  394. * @phpstan-param DqlToken|null $token
  395. *
  396. * @return void
  397. * @phpstan-return no-return
  398. *
  399. * @throws QueryException
  400. */
  401. public function syntaxError($expected = '', $token = null)
  402. {
  403. if ($token === null) {
  404. $token = $this->lexer->lookahead;
  405. }
  406. $tokenPos = $token->position ?? '-1';
  407. $message = sprintf('line 0, col %d: Error: ', $tokenPos);
  408. $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';
  409. $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value);
  410. throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? ''));
  411. }
  412. /**
  413. * Generates a new semantical error.
  414. *
  415. * @param string $message Optional message.
  416. * @param mixed[]|null $token Optional token.
  417. * @phpstan-param DqlToken|null $token
  418. *
  419. * @return void
  420. * @phpstan-return no-return
  421. *
  422. * @throws QueryException
  423. */
  424. public function semanticalError($message = '', $token = null)
  425. {
  426. if ($token === null) {
  427. $token = $this->lexer->lookahead ?? new Token('fake token', 42, 0);
  428. }
  429. // Minimum exposed chars ahead of token
  430. $distance = 12;
  431. // Find a position of a final word to display in error string
  432. $dql = $this->query->getDQL();
  433. $length = strlen($dql);
  434. $pos = $token->position + $distance;
  435. $pos = strpos($dql, ' ', $length > $pos ? $pos : $length);
  436. $length = $pos !== false ? $pos - $token->position : $distance;
  437. $tokenPos = $token->position > 0 ? $token->position : '-1';
  438. $tokenStr = substr($dql, $token->position, $length);
  439. // Building informative message
  440. $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
  441. throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
  442. }
  443. /**
  444. * Peeks beyond the matched closing parenthesis and returns the first token after that one.
  445. *
  446. * @param bool $resetPeek Reset peek after finding the closing parenthesis.
  447. *
  448. * @return mixed[]
  449. * @phpstan-return DqlToken|null
  450. */
  451. private function peekBeyondClosingParenthesis(bool $resetPeek = true)
  452. {
  453. $token = $this->lexer->peek();
  454. $numUnmatched = 1;
  455. while ($numUnmatched > 0 && $token !== null) {
  456. switch ($token->type) {
  457. case TokenType::T_OPEN_PARENTHESIS:
  458. ++$numUnmatched;
  459. break;
  460. case TokenType::T_CLOSE_PARENTHESIS:
  461. --$numUnmatched;
  462. break;
  463. default:
  464. // Do nothing
  465. }
  466. $token = $this->lexer->peek();
  467. }
  468. if ($resetPeek) {
  469. $this->lexer->resetPeek();
  470. }
  471. return $token;
  472. }
  473. /**
  474. * Checks if the given token indicates a mathematical operator.
  475. *
  476. * @phpstan-param DqlToken|null $token
  477. */
  478. private function isMathOperator($token): bool
  479. {
  480. return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true);
  481. }
  482. /**
  483. * Checks if the next-next (after lookahead) token starts a function.
  484. *
  485. * @return bool TRUE if the next-next tokens start a function, FALSE otherwise.
  486. */
  487. private function isFunction(): bool
  488. {
  489. assert($this->lexer->lookahead !== null);
  490. $lookaheadType = $this->lexer->lookahead->type;
  491. $peek = $this->lexer->peek();
  492. $this->lexer->resetPeek();
  493. return $lookaheadType >= TokenType::T_IDENTIFIER && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS;
  494. }
  495. /**
  496. * Checks whether the given token type indicates an aggregate function.
  497. *
  498. * @phpstan-param TokenType::T_* $tokenType
  499. *
  500. * @return bool TRUE if the token type is an aggregate function, FALSE otherwise.
  501. */
  502. private function isAggregateFunction(int $tokenType): bool
  503. {
  504. return in_array(
  505. $tokenType,
  506. [TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT],
  507. true
  508. );
  509. }
  510. /**
  511. * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
  512. */
  513. private function isNextAllAnySome(): bool
  514. {
  515. assert($this->lexer->lookahead !== null);
  516. return in_array(
  517. $this->lexer->lookahead->type,
  518. [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME],
  519. true
  520. );
  521. }
  522. /**
  523. * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
  524. * It must exist in query components list.
  525. */
  526. private function processDeferredIdentificationVariables(): void
  527. {
  528. foreach ($this->deferredIdentificationVariables as $deferredItem) {
  529. $identVariable = $deferredItem['expression'];
  530. // Check if IdentificationVariable exists in queryComponents
  531. if (! isset($this->queryComponents[$identVariable])) {
  532. $this->semanticalError(
  533. sprintf("'%s' is not defined.", $identVariable),
  534. $deferredItem['token']
  535. );
  536. }
  537. $qComp = $this->queryComponents[$identVariable];
  538. // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
  539. if (! isset($qComp['metadata'])) {
  540. $this->semanticalError(
  541. sprintf("'%s' does not point to a Class.", $identVariable),
  542. $deferredItem['token']
  543. );
  544. }
  545. // Validate if identification variable nesting level is lower or equal than the current one
  546. if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
  547. $this->semanticalError(
  548. sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
  549. $deferredItem['token']
  550. );
  551. }
  552. }
  553. }
  554. /**
  555. * Validates that the given <tt>NewObjectExpression</tt>.
  556. */
  557. private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void
  558. {
  559. foreach ($this->deferredNewObjectExpressions as $deferredItem) {
  560. $expression = $deferredItem['expression'];
  561. $token = $deferredItem['token'];
  562. $className = $expression->className;
  563. $args = $expression->args;
  564. $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;
  565. // If the namespace is not given then assumes the first FROM entity namespace
  566. if (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) {
  567. $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
  568. $fqcn = $namespace . '\\' . $className;
  569. if (class_exists($fqcn)) {
  570. $expression->className = $fqcn;
  571. $className = $fqcn;
  572. }
  573. }
  574. if (! class_exists($className)) {
  575. $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
  576. }
  577. $class = new ReflectionClass($className);
  578. if (! $class->isInstantiable()) {
  579. $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
  580. }
  581. if ($class->getConstructor() === null) {
  582. $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
  583. }
  584. if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
  585. $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
  586. }
  587. }
  588. }
  589. /**
  590. * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
  591. * It must exist in query components list.
  592. */
  593. private function processDeferredPartialObjectExpressions(): void
  594. {
  595. foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
  596. $expr = $deferredItem['expression'];
  597. $class = $this->getMetadataForDqlAlias($expr->identificationVariable);
  598. foreach ($expr->partialFieldSet as $field) {
  599. if (isset($class->fieldMappings[$field])) {
  600. continue;
  601. }
  602. if (
  603. isset($class->associationMappings[$field]) &&
  604. $class->associationMappings[$field]['isOwningSide'] &&
  605. $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE
  606. ) {
  607. continue;
  608. }
  609. $this->semanticalError(sprintf(
  610. "There is no mapped field named '%s' on class %s.",
  611. $field,
  612. $class->name
  613. ), $deferredItem['token']);
  614. }
  615. if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
  616. $this->semanticalError(
  617. 'The partial field selection of class ' . $class->name . ' must contain the identifier.',
  618. $deferredItem['token']
  619. );
  620. }
  621. }
  622. }
  623. /**
  624. * Validates that the given <tt>ResultVariable</tt> is semantically correct.
  625. * It must exist in query components list.
  626. */
  627. private function processDeferredResultVariables(): void
  628. {
  629. foreach ($this->deferredResultVariables as $deferredItem) {
  630. $resultVariable = $deferredItem['expression'];
  631. // Check if ResultVariable exists in queryComponents
  632. if (! isset($this->queryComponents[$resultVariable])) {
  633. $this->semanticalError(
  634. sprintf("'%s' is not defined.", $resultVariable),
  635. $deferredItem['token']
  636. );
  637. }
  638. $qComp = $this->queryComponents[$resultVariable];
  639. // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
  640. if (! isset($qComp['resultVariable'])) {
  641. $this->semanticalError(
  642. sprintf("'%s' does not point to a ResultVariable.", $resultVariable),
  643. $deferredItem['token']
  644. );
  645. }
  646. // Validate if identification variable nesting level is lower or equal than the current one
  647. if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
  648. $this->semanticalError(
  649. sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),
  650. $deferredItem['token']
  651. );
  652. }
  653. }
  654. }
  655. /**
  656. * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
  657. *
  658. * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
  659. * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
  660. * StateFieldPathExpression ::= IdentificationVariable "." StateField
  661. * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
  662. * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
  663. */
  664. private function processDeferredPathExpressions(): void
  665. {
  666. foreach ($this->deferredPathExpressions as $deferredItem) {
  667. $pathExpression = $deferredItem['expression'];
  668. $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable);
  669. $field = $pathExpression->field;
  670. if ($field === null) {
  671. $field = $pathExpression->field = $class->identifier[0];
  672. }
  673. // Check if field or association exists
  674. if (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
  675. $this->semanticalError(
  676. 'Class ' . $class->name . ' has no field or association named ' . $field,
  677. $deferredItem['token']
  678. );
  679. }
  680. $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
  681. if (isset($class->associationMappings[$field])) {
  682. $assoc = $class->associationMappings[$field];
  683. $fieldType = $assoc['type'] & ClassMetadata::TO_ONE
  684. ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
  685. : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
  686. }
  687. // Validate if PathExpression is one of the expected types
  688. $expectedType = $pathExpression->expectedType;
  689. if (! ($expectedType & $fieldType)) {
  690. // We need to recognize which was expected type(s)
  691. $expectedStringTypes = [];
  692. // Validate state field type
  693. if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
  694. $expectedStringTypes[] = 'StateFieldPathExpression';
  695. }
  696. // Validate single valued association (*-to-one)
  697. if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
  698. $expectedStringTypes[] = 'SingleValuedAssociationField';
  699. }
  700. // Validate single valued association (*-to-many)
  701. if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
  702. $expectedStringTypes[] = 'CollectionValuedAssociationField';
  703. }
  704. // Build the error message
  705. $semanticalError = 'Invalid PathExpression. ';
  706. $semanticalError .= count($expectedStringTypes) === 1
  707. ? 'Must be a ' . $expectedStringTypes[0] . '.'
  708. : implode(' or ', $expectedStringTypes) . ' expected.';
  709. $this->semanticalError($semanticalError, $deferredItem['token']);
  710. }
  711. // We need to force the type in PathExpression
  712. $pathExpression->type = $fieldType;
  713. }
  714. }
  715. private function processRootEntityAliasSelected(): void
  716. {
  717. if (! count($this->identVariableExpressions)) {
  718. return;
  719. }
  720. foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
  721. if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) {
  722. return;
  723. }
  724. }
  725. $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
  726. }
  727. /**
  728. * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
  729. *
  730. * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
  731. */
  732. public function QueryLanguage()
  733. {
  734. $statement = null;
  735. $this->lexer->moveNext();
  736. switch ($this->lexer->lookahead->type ?? null) {
  737. case TokenType::T_SELECT:
  738. $statement = $this->SelectStatement();
  739. break;
  740. case TokenType::T_UPDATE:
  741. $statement = $this->UpdateStatement();
  742. break;
  743. case TokenType::T_DELETE:
  744. $statement = $this->DeleteStatement();
  745. break;
  746. default:
  747. $this->syntaxError('SELECT, UPDATE or DELETE');
  748. break;
  749. }
  750. // Check for end of string
  751. if ($this->lexer->lookahead !== null) {
  752. $this->syntaxError('end of string');
  753. }
  754. return $statement;
  755. }
  756. /**
  757. * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
  758. *
  759. * @return AST\SelectStatement
  760. */
  761. public function SelectStatement()
  762. {
  763. $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
  764. $selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
  765. $selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null;
  766. $selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null;
  767. $selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null;
  768. return $selectStatement;
  769. }
  770. /**
  771. * UpdateStatement ::= UpdateClause [WhereClause]
  772. *
  773. * @return AST\UpdateStatement
  774. */
  775. public function UpdateStatement()
  776. {
  777. $updateStatement = new AST\UpdateStatement($this->UpdateClause());
  778. $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
  779. return $updateStatement;
  780. }
  781. /**
  782. * DeleteStatement ::= DeleteClause [WhereClause]
  783. *
  784. * @return AST\DeleteStatement
  785. */
  786. public function DeleteStatement()
  787. {
  788. $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
  789. $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
  790. return $deleteStatement;
  791. }
  792. /**
  793. * IdentificationVariable ::= identifier
  794. *
  795. * @return string
  796. */
  797. public function IdentificationVariable()
  798. {
  799. $this->match(TokenType::T_IDENTIFIER);
  800. assert($this->lexer->token !== null);
  801. $identVariable = $this->lexer->token->value;
  802. $this->deferredIdentificationVariables[] = [
  803. 'expression' => $identVariable,
  804. 'nestingLevel' => $this->nestingLevel,
  805. 'token' => $this->lexer->token,
  806. ];
  807. return $identVariable;
  808. }
  809. /**
  810. * AliasIdentificationVariable = identifier
  811. *
  812. * @return string
  813. */
  814. public function AliasIdentificationVariable()
  815. {
  816. $this->match(TokenType::T_IDENTIFIER);
  817. assert($this->lexer->token !== null);
  818. $aliasIdentVariable = $this->lexer->token->value;
  819. $exists = isset($this->queryComponents[$aliasIdentVariable]);
  820. if ($exists) {
  821. $this->semanticalError(
  822. sprintf("'%s' is already defined.", $aliasIdentVariable),
  823. $this->lexer->token
  824. );
  825. }
  826. return $aliasIdentVariable;
  827. }
  828. /**
  829. * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
  830. *
  831. * @return string
  832. */
  833. public function AbstractSchemaName()
  834. {
  835. if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) {
  836. $this->match(TokenType::T_FULLY_QUALIFIED_NAME);
  837. assert($this->lexer->token !== null);
  838. return $this->lexer->token->value;
  839. }
  840. if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
  841. $this->match(TokenType::T_IDENTIFIER);
  842. assert($this->lexer->token !== null);
  843. return $this->lexer->token->value;
  844. }
  845. // @phpstan-ignore classConstant.deprecated
  846. $this->match(TokenType::T_ALIASED_NAME);
  847. assert($this->lexer->token !== null);
  848. Deprecation::trigger(
  849. 'doctrine/orm',
  850. 'https://github.com/doctrine/orm/issues/8818',
  851. 'Short namespace aliases such as "%s" are deprecated and will be removed in Doctrine ORM 3.0.',
  852. $this->lexer->token->value
  853. );
  854. [$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token->value);
  855. return $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
  856. }
  857. /**
  858. * Validates an AbstractSchemaName, making sure the class exists.
  859. *
  860. * @param string $schemaName The name to validate.
  861. *
  862. * @throws QueryException if the name does not exist.
  863. */
  864. private function validateAbstractSchemaName(string $schemaName): void
  865. {
  866. assert($this->lexer->token !== null);
  867. if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
  868. $this->semanticalError(
  869. sprintf("Class '%s' is not defined.", $schemaName),
  870. $this->lexer->token
  871. );
  872. }
  873. }
  874. /**
  875. * AliasResultVariable ::= identifier
  876. *
  877. * @return string
  878. */
  879. public function AliasResultVariable()
  880. {
  881. $this->match(TokenType::T_IDENTIFIER);
  882. assert($this->lexer->token !== null);
  883. $resultVariable = $this->lexer->token->value;
  884. $exists = isset($this->queryComponents[$resultVariable]);
  885. if ($exists) {
  886. $this->semanticalError(
  887. sprintf("'%s' is already defined.", $resultVariable),
  888. $this->lexer->token
  889. );
  890. }
  891. return $resultVariable;
  892. }
  893. /**
  894. * ResultVariable ::= identifier
  895. *
  896. * @return string
  897. */
  898. public function ResultVariable()
  899. {
  900. $this->match(TokenType::T_IDENTIFIER);
  901. assert($this->lexer->token !== null);
  902. $resultVariable = $this->lexer->token->value;
  903. // Defer ResultVariable validation
  904. $this->deferredResultVariables[] = [
  905. 'expression' => $resultVariable,
  906. 'nestingLevel' => $this->nestingLevel,
  907. 'token' => $this->lexer->token,
  908. ];
  909. return $resultVariable;
  910. }
  911. /**
  912. * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
  913. *
  914. * @return AST\JoinAssociationPathExpression
  915. */
  916. public function JoinAssociationPathExpression()
  917. {
  918. $identVariable = $this->IdentificationVariable();
  919. if (! isset($this->queryComponents[$identVariable])) {
  920. $this->semanticalError(
  921. 'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.'
  922. );
  923. }
  924. $this->match(TokenType::T_DOT);
  925. $this->match(TokenType::T_IDENTIFIER);
  926. assert($this->lexer->token !== null);
  927. $field = $this->lexer->token->value;
  928. // Validate association field
  929. $class = $this->getMetadataForDqlAlias($identVariable);
  930. if (! $class->hasAssociation($field)) {
  931. $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
  932. }
  933. return new AST\JoinAssociationPathExpression($identVariable, $field);
  934. }
  935. /**
  936. * Parses an arbitrary path expression and defers semantical validation
  937. * based on expected types.
  938. *
  939. * PathExpression ::= IdentificationVariable {"." identifier}*
  940. *
  941. * @param int $expectedTypes
  942. * @phpstan-param int-mask-of<AST\PathExpression::TYPE_*> $expectedTypes
  943. *
  944. * @return AST\PathExpression
  945. */
  946. public function PathExpression($expectedTypes)
  947. {
  948. $identVariable = $this->IdentificationVariable();
  949. $field = null;
  950. assert($this->lexer->token !== null);
  951. if ($this->lexer->isNextToken(TokenType::T_DOT)) {
  952. $this->match(TokenType::T_DOT);
  953. $this->match(TokenType::T_IDENTIFIER);
  954. $field = $this->lexer->token->value;
  955. while ($this->lexer->isNextToken(TokenType::T_DOT)) {
  956. $this->match(TokenType::T_DOT);
  957. $this->match(TokenType::T_IDENTIFIER);
  958. $field .= '.' . $this->lexer->token->value;
  959. }
  960. }
  961. // Creating AST node
  962. $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
  963. // Defer PathExpression validation if requested to be deferred
  964. $this->deferredPathExpressions[] = [
  965. 'expression' => $pathExpr,
  966. 'nestingLevel' => $this->nestingLevel,
  967. 'token' => $this->lexer->token,
  968. ];
  969. return $pathExpr;
  970. }
  971. /**
  972. * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
  973. *
  974. * @return AST\PathExpression
  975. */
  976. public function AssociationPathExpression()
  977. {
  978. return $this->PathExpression(
  979. AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
  980. AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
  981. );
  982. }
  983. /**
  984. * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
  985. *
  986. * @return AST\PathExpression
  987. */
  988. public function SingleValuedPathExpression()
  989. {
  990. return $this->PathExpression(
  991. AST\PathExpression::TYPE_STATE_FIELD |
  992. AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
  993. );
  994. }
  995. /**
  996. * StateFieldPathExpression ::= IdentificationVariable "." StateField
  997. *
  998. * @return AST\PathExpression
  999. */
  1000. public function StateFieldPathExpression()
  1001. {
  1002. return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
  1003. }
  1004. /**
  1005. * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
  1006. *
  1007. * @return AST\PathExpression
  1008. */
  1009. public function SingleValuedAssociationPathExpression()
  1010. {
  1011. return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
  1012. }
  1013. /**
  1014. * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
  1015. *
  1016. * @return AST\PathExpression
  1017. */
  1018. public function CollectionValuedPathExpression()
  1019. {
  1020. return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
  1021. }
  1022. /**
  1023. * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
  1024. *
  1025. * @return AST\SelectClause
  1026. */
  1027. public function SelectClause()
  1028. {
  1029. $isDistinct = false;
  1030. $this->match(TokenType::T_SELECT);
  1031. // Check for DISTINCT
  1032. if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {
  1033. $this->match(TokenType::T_DISTINCT);
  1034. $isDistinct = true;
  1035. }
  1036. // Process SelectExpressions (1..N)
  1037. $selectExpressions = [];
  1038. $selectExpressions[] = $this->SelectExpression();
  1039. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1040. $this->match(TokenType::T_COMMA);
  1041. $selectExpressions[] = $this->SelectExpression();
  1042. }
  1043. return new AST\SelectClause($selectExpressions, $isDistinct);
  1044. }
  1045. /**
  1046. * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
  1047. *
  1048. * @return AST\SimpleSelectClause
  1049. */
  1050. public function SimpleSelectClause()
  1051. {
  1052. $isDistinct = false;
  1053. $this->match(TokenType::T_SELECT);
  1054. if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {
  1055. $this->match(TokenType::T_DISTINCT);
  1056. $isDistinct = true;
  1057. }
  1058. return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
  1059. }
  1060. /**
  1061. * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
  1062. *
  1063. * @return AST\UpdateClause
  1064. */
  1065. public function UpdateClause()
  1066. {
  1067. $this->match(TokenType::T_UPDATE);
  1068. assert($this->lexer->lookahead !== null);
  1069. $token = $this->lexer->lookahead;
  1070. $abstractSchemaName = $this->AbstractSchemaName();
  1071. $this->validateAbstractSchemaName($abstractSchemaName);
  1072. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1073. $this->match(TokenType::T_AS);
  1074. }
  1075. $aliasIdentificationVariable = $this->AliasIdentificationVariable();
  1076. $class = $this->em->getClassMetadata($abstractSchemaName);
  1077. // Building queryComponent
  1078. $queryComponent = [
  1079. 'metadata' => $class,
  1080. 'parent' => null,
  1081. 'relation' => null,
  1082. 'map' => null,
  1083. 'nestingLevel' => $this->nestingLevel,
  1084. 'token' => $token,
  1085. ];
  1086. $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
  1087. $this->match(TokenType::T_SET);
  1088. $updateItems = [];
  1089. $updateItems[] = $this->UpdateItem();
  1090. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1091. $this->match(TokenType::T_COMMA);
  1092. $updateItems[] = $this->UpdateItem();
  1093. }
  1094. $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
  1095. $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
  1096. return $updateClause;
  1097. }
  1098. /**
  1099. * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
  1100. *
  1101. * @return AST\DeleteClause
  1102. */
  1103. public function DeleteClause()
  1104. {
  1105. $this->match(TokenType::T_DELETE);
  1106. if ($this->lexer->isNextToken(TokenType::T_FROM)) {
  1107. $this->match(TokenType::T_FROM);
  1108. }
  1109. assert($this->lexer->lookahead !== null);
  1110. $token = $this->lexer->lookahead;
  1111. $abstractSchemaName = $this->AbstractSchemaName();
  1112. $this->validateAbstractSchemaName($abstractSchemaName);
  1113. $deleteClause = new AST\DeleteClause($abstractSchemaName);
  1114. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1115. $this->match(TokenType::T_AS);
  1116. }
  1117. $aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER)
  1118. ? $this->AliasIdentificationVariable()
  1119. : 'alias_should_have_been_set';
  1120. $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
  1121. $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
  1122. // Building queryComponent
  1123. $queryComponent = [
  1124. 'metadata' => $class,
  1125. 'parent' => null,
  1126. 'relation' => null,
  1127. 'map' => null,
  1128. 'nestingLevel' => $this->nestingLevel,
  1129. 'token' => $token,
  1130. ];
  1131. $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
  1132. return $deleteClause;
  1133. }
  1134. /**
  1135. * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
  1136. *
  1137. * @return AST\FromClause
  1138. */
  1139. public function FromClause()
  1140. {
  1141. $this->match(TokenType::T_FROM);
  1142. $identificationVariableDeclarations = [];
  1143. $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
  1144. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1145. $this->match(TokenType::T_COMMA);
  1146. $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
  1147. }
  1148. return new AST\FromClause($identificationVariableDeclarations);
  1149. }
  1150. /**
  1151. * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
  1152. *
  1153. * @return AST\SubselectFromClause
  1154. */
  1155. public function SubselectFromClause()
  1156. {
  1157. $this->match(TokenType::T_FROM);
  1158. $identificationVariables = [];
  1159. $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
  1160. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1161. $this->match(TokenType::T_COMMA);
  1162. $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
  1163. }
  1164. return new AST\SubselectFromClause($identificationVariables);
  1165. }
  1166. /**
  1167. * WhereClause ::= "WHERE" ConditionalExpression
  1168. *
  1169. * @return AST\WhereClause
  1170. */
  1171. public function WhereClause()
  1172. {
  1173. $this->match(TokenType::T_WHERE);
  1174. return new AST\WhereClause($this->ConditionalExpression());
  1175. }
  1176. /**
  1177. * HavingClause ::= "HAVING" ConditionalExpression
  1178. *
  1179. * @return AST\HavingClause
  1180. */
  1181. public function HavingClause()
  1182. {
  1183. $this->match(TokenType::T_HAVING);
  1184. return new AST\HavingClause($this->ConditionalExpression());
  1185. }
  1186. /**
  1187. * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
  1188. *
  1189. * @return AST\GroupByClause
  1190. */
  1191. public function GroupByClause()
  1192. {
  1193. $this->match(TokenType::T_GROUP);
  1194. $this->match(TokenType::T_BY);
  1195. $groupByItems = [$this->GroupByItem()];
  1196. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1197. $this->match(TokenType::T_COMMA);
  1198. $groupByItems[] = $this->GroupByItem();
  1199. }
  1200. return new AST\GroupByClause($groupByItems);
  1201. }
  1202. /**
  1203. * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
  1204. *
  1205. * @return AST\OrderByClause
  1206. */
  1207. public function OrderByClause()
  1208. {
  1209. $this->match(TokenType::T_ORDER);
  1210. $this->match(TokenType::T_BY);
  1211. $orderByItems = [];
  1212. $orderByItems[] = $this->OrderByItem();
  1213. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1214. $this->match(TokenType::T_COMMA);
  1215. $orderByItems[] = $this->OrderByItem();
  1216. }
  1217. return new AST\OrderByClause($orderByItems, $this->withJoinConditionNestingLevel === 0);
  1218. }
  1219. /**
  1220. * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
  1221. *
  1222. * @return AST\Subselect
  1223. */
  1224. public function Subselect()
  1225. {
  1226. // Increase query nesting level
  1227. $this->nestingLevel++;
  1228. $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
  1229. $subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
  1230. $subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null;
  1231. $subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null;
  1232. $subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null;
  1233. // Decrease query nesting level
  1234. $this->nestingLevel--;
  1235. return $subselect;
  1236. }
  1237. /**
  1238. * UpdateItem ::= SingleValuedPathExpression "=" NewValue
  1239. *
  1240. * @return AST\UpdateItem
  1241. */
  1242. public function UpdateItem()
  1243. {
  1244. $pathExpr = $this->SingleValuedPathExpression();
  1245. $this->match(TokenType::T_EQUALS);
  1246. return new AST\UpdateItem($pathExpr, $this->NewValue());
  1247. }
  1248. /**
  1249. * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
  1250. *
  1251. * @return string|AST\PathExpression
  1252. */
  1253. public function GroupByItem()
  1254. {
  1255. // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
  1256. $glimpse = $this->lexer->glimpse();
  1257. if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) {
  1258. return $this->SingleValuedPathExpression();
  1259. }
  1260. assert($this->lexer->lookahead !== null);
  1261. // Still need to decide between IdentificationVariable or ResultVariable
  1262. $lookaheadValue = $this->lexer->lookahead->value;
  1263. if (! isset($this->queryComponents[$lookaheadValue])) {
  1264. $this->semanticalError('Cannot group by undefined identification or result variable.');
  1265. }
  1266. return isset($this->queryComponents[$lookaheadValue]['metadata'])
  1267. ? $this->IdentificationVariable()
  1268. : $this->ResultVariable();
  1269. }
  1270. /**
  1271. * OrderByItem ::= (
  1272. * SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression |
  1273. * ScalarExpression | ResultVariable | FunctionDeclaration
  1274. * ) ["ASC" | "DESC"]
  1275. *
  1276. * @return AST\OrderByItem
  1277. */
  1278. public function OrderByItem()
  1279. {
  1280. $this->lexer->peek(); // lookahead => '.'
  1281. $this->lexer->peek(); // lookahead => token after '.'
  1282. $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
  1283. $this->lexer->resetPeek();
  1284. $glimpse = $this->lexer->glimpse();
  1285. assert($this->lexer->lookahead !== null);
  1286. switch (true) {
  1287. case $this->isMathOperator($peek) || $this->isMathOperator($glimpse):
  1288. $expr = $this->SimpleArithmeticExpression();
  1289. break;
  1290. case $glimpse !== null && $glimpse->type === TokenType::T_DOT:
  1291. $expr = $this->SingleValuedPathExpression();
  1292. break;
  1293. case $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()):
  1294. $expr = $this->ScalarExpression();
  1295. break;
  1296. case $this->lexer->lookahead->type === TokenType::T_CASE:
  1297. $expr = $this->CaseExpression();
  1298. break;
  1299. case $this->isFunction():
  1300. $expr = $this->FunctionDeclaration();
  1301. break;
  1302. default:
  1303. $expr = $this->ResultVariable();
  1304. break;
  1305. }
  1306. $type = 'ASC';
  1307. $item = new AST\OrderByItem($expr);
  1308. switch (true) {
  1309. case $this->lexer->isNextToken(TokenType::T_DESC):
  1310. $this->match(TokenType::T_DESC);
  1311. $type = 'DESC';
  1312. break;
  1313. case $this->lexer->isNextToken(TokenType::T_ASC):
  1314. $this->match(TokenType::T_ASC);
  1315. break;
  1316. default:
  1317. // Do nothing
  1318. }
  1319. $item->type = $type;
  1320. return $item;
  1321. }
  1322. /**
  1323. * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
  1324. * EnumPrimary | SimpleEntityExpression | "NULL"
  1325. *
  1326. * NOTE: Since it is not possible to correctly recognize individual types, here is the full
  1327. * grammar that needs to be supported:
  1328. *
  1329. * NewValue ::= SimpleArithmeticExpression | "NULL"
  1330. *
  1331. * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
  1332. *
  1333. * @return AST\ArithmeticExpression|AST\InputParameter|null
  1334. */
  1335. public function NewValue()
  1336. {
  1337. if ($this->lexer->isNextToken(TokenType::T_NULL)) {
  1338. $this->match(TokenType::T_NULL);
  1339. return null;
  1340. }
  1341. if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
  1342. $this->match(TokenType::T_INPUT_PARAMETER);
  1343. assert($this->lexer->token !== null);
  1344. return new AST\InputParameter($this->lexer->token->value);
  1345. }
  1346. return $this->ArithmeticExpression();
  1347. }
  1348. /**
  1349. * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
  1350. *
  1351. * @return AST\IdentificationVariableDeclaration
  1352. */
  1353. public function IdentificationVariableDeclaration()
  1354. {
  1355. $joins = [];
  1356. $rangeVariableDeclaration = $this->RangeVariableDeclaration();
  1357. $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX)
  1358. ? $this->IndexBy()
  1359. : null;
  1360. $rangeVariableDeclaration->isRoot = true;
  1361. while (
  1362. $this->lexer->isNextToken(TokenType::T_LEFT) ||
  1363. $this->lexer->isNextToken(TokenType::T_INNER) ||
  1364. $this->lexer->isNextToken(TokenType::T_JOIN)
  1365. ) {
  1366. $joins[] = $this->Join();
  1367. }
  1368. return new AST\IdentificationVariableDeclaration(
  1369. $rangeVariableDeclaration,
  1370. $indexBy,
  1371. $joins
  1372. );
  1373. }
  1374. /**
  1375. * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
  1376. *
  1377. * {Internal note: WARNING: Solution is harder than a bare implementation.
  1378. * Desired EBNF support:
  1379. *
  1380. * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
  1381. *
  1382. * It demands that entire SQL generation to become programmatical. This is
  1383. * needed because association based subselect requires "WHERE" conditional
  1384. * expressions to be injected, but there is no scope to do that. Only scope
  1385. * accessible is "FROM", prohibiting an easy implementation without larger
  1386. * changes.}
  1387. *
  1388. * @return AST\IdentificationVariableDeclaration
  1389. */
  1390. public function SubselectIdentificationVariableDeclaration()
  1391. {
  1392. /*
  1393. NOT YET IMPLEMENTED!
  1394. $glimpse = $this->lexer->glimpse();
  1395. if ($glimpse->type == TokenType::T_DOT) {
  1396. $associationPathExpression = $this->AssociationPathExpression();
  1397. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1398. $this->match(TokenType::T_AS);
  1399. }
  1400. $aliasIdentificationVariable = $this->AliasIdentificationVariable();
  1401. $identificationVariable = $associationPathExpression->identificationVariable;
  1402. $field = $associationPathExpression->associationField;
  1403. $class = $this->queryComponents[$identificationVariable]['metadata'];
  1404. $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
  1405. // Building queryComponent
  1406. $joinQueryComponent = array(
  1407. 'metadata' => $targetClass,
  1408. 'parent' => $identificationVariable,
  1409. 'relation' => $class->getAssociationMapping($field),
  1410. 'map' => null,
  1411. 'nestingLevel' => $this->nestingLevel,
  1412. 'token' => $this->lexer->lookahead
  1413. );
  1414. $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
  1415. return new AST\SubselectIdentificationVariableDeclaration(
  1416. $associationPathExpression, $aliasIdentificationVariable
  1417. );
  1418. }
  1419. */
  1420. return $this->IdentificationVariableDeclaration();
  1421. }
  1422. /**
  1423. * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
  1424. * (JoinAssociationDeclaration | RangeVariableDeclaration)
  1425. * ["WITH" ConditionalExpression]
  1426. *
  1427. * @return AST\Join
  1428. */
  1429. public function Join()
  1430. {
  1431. // Check Join type
  1432. $joinType = AST\Join::JOIN_TYPE_INNER;
  1433. switch (true) {
  1434. case $this->lexer->isNextToken(TokenType::T_LEFT):
  1435. $this->match(TokenType::T_LEFT);
  1436. $joinType = AST\Join::JOIN_TYPE_LEFT;
  1437. // Possible LEFT OUTER join
  1438. if ($this->lexer->isNextToken(TokenType::T_OUTER)) {
  1439. $this->match(TokenType::T_OUTER);
  1440. $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
  1441. }
  1442. break;
  1443. case $this->lexer->isNextToken(TokenType::T_INNER):
  1444. $this->match(TokenType::T_INNER);
  1445. break;
  1446. default:
  1447. // Do nothing
  1448. }
  1449. $this->match(TokenType::T_JOIN);
  1450. $next = $this->lexer->glimpse();
  1451. assert($next !== null);
  1452. $joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
  1453. $adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH);
  1454. $join = new AST\Join($joinType, $joinDeclaration);
  1455. // Describe non-root join declaration
  1456. if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
  1457. $joinDeclaration->isRoot = false;
  1458. }
  1459. // Check for ad-hoc Join conditions
  1460. if ($adhocConditions) {
  1461. $this->match(TokenType::T_WITH);
  1462. try {
  1463. $this->withJoinConditionNestingLevel++;
  1464. $join->conditionalExpression = $this->ConditionalExpression();
  1465. } finally {
  1466. $this->withJoinConditionNestingLevel--;
  1467. }
  1468. }
  1469. return $join;
  1470. }
  1471. /**
  1472. * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
  1473. *
  1474. * @return AST\RangeVariableDeclaration
  1475. *
  1476. * @throws QueryException
  1477. */
  1478. public function RangeVariableDeclaration()
  1479. {
  1480. if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) {
  1481. $this->semanticalError('Subquery is not supported here', $this->lexer->token);
  1482. }
  1483. $abstractSchemaName = $this->AbstractSchemaName();
  1484. $this->validateAbstractSchemaName($abstractSchemaName);
  1485. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1486. $this->match(TokenType::T_AS);
  1487. }
  1488. assert($this->lexer->lookahead !== null);
  1489. $token = $this->lexer->lookahead;
  1490. $aliasIdentificationVariable = $this->AliasIdentificationVariable();
  1491. $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
  1492. // Building queryComponent
  1493. $queryComponent = [
  1494. 'metadata' => $classMetadata,
  1495. 'parent' => null,
  1496. 'relation' => null,
  1497. 'map' => null,
  1498. 'nestingLevel' => $this->nestingLevel,
  1499. 'token' => $token,
  1500. ];
  1501. $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
  1502. return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
  1503. }
  1504. /**
  1505. * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
  1506. *
  1507. * @return AST\JoinAssociationDeclaration
  1508. */
  1509. public function JoinAssociationDeclaration()
  1510. {
  1511. $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
  1512. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1513. $this->match(TokenType::T_AS);
  1514. }
  1515. assert($this->lexer->lookahead !== null);
  1516. $aliasIdentificationVariable = $this->AliasIdentificationVariable();
  1517. $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null;
  1518. $identificationVariable = $joinAssociationPathExpression->identificationVariable;
  1519. $field = $joinAssociationPathExpression->associationField;
  1520. $class = $this->getMetadataForDqlAlias($identificationVariable);
  1521. $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
  1522. // Building queryComponent
  1523. $joinQueryComponent = [
  1524. 'metadata' => $targetClass,
  1525. 'parent' => $joinAssociationPathExpression->identificationVariable,
  1526. 'relation' => $class->getAssociationMapping($field),
  1527. 'map' => null,
  1528. 'nestingLevel' => $this->nestingLevel,
  1529. 'token' => $this->lexer->lookahead,
  1530. ];
  1531. $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
  1532. return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
  1533. }
  1534. /**
  1535. * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
  1536. * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
  1537. *
  1538. * @return AST\PartialObjectExpression
  1539. */
  1540. public function PartialObjectExpression()
  1541. {
  1542. $this->match(TokenType::T_PARTIAL);
  1543. $partialFieldSet = [];
  1544. $identificationVariable = $this->IdentificationVariable();
  1545. $this->match(TokenType::T_DOT);
  1546. $this->match(TokenType::T_OPEN_CURLY_BRACE);
  1547. $this->match(TokenType::T_IDENTIFIER);
  1548. assert($this->lexer->token !== null);
  1549. $field = $this->lexer->token->value;
  1550. // First field in partial expression might be embeddable property
  1551. while ($this->lexer->isNextToken(TokenType::T_DOT)) {
  1552. $this->match(TokenType::T_DOT);
  1553. $this->match(TokenType::T_IDENTIFIER);
  1554. $field .= '.' . $this->lexer->token->value;
  1555. }
  1556. $partialFieldSet[] = $field;
  1557. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1558. $this->match(TokenType::T_COMMA);
  1559. $this->match(TokenType::T_IDENTIFIER);
  1560. $field = $this->lexer->token->value;
  1561. while ($this->lexer->isNextToken(TokenType::T_DOT)) {
  1562. $this->match(TokenType::T_DOT);
  1563. $this->match(TokenType::T_IDENTIFIER);
  1564. $field .= '.' . $this->lexer->token->value;
  1565. }
  1566. $partialFieldSet[] = $field;
  1567. }
  1568. $this->match(TokenType::T_CLOSE_CURLY_BRACE);
  1569. $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
  1570. // Defer PartialObjectExpression validation
  1571. $this->deferredPartialObjectExpressions[] = [
  1572. 'expression' => $partialObjectExpression,
  1573. 'nestingLevel' => $this->nestingLevel,
  1574. 'token' => $this->lexer->token,
  1575. ];
  1576. return $partialObjectExpression;
  1577. }
  1578. /**
  1579. * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
  1580. *
  1581. * @return AST\NewObjectExpression
  1582. */
  1583. public function NewObjectExpression()
  1584. {
  1585. $this->match(TokenType::T_NEW);
  1586. $className = $this->AbstractSchemaName(); // note that this is not yet validated
  1587. $token = $this->lexer->token;
  1588. $this->match(TokenType::T_OPEN_PARENTHESIS);
  1589. $args[] = $this->NewObjectArg();
  1590. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1591. $this->match(TokenType::T_COMMA);
  1592. $args[] = $this->NewObjectArg();
  1593. }
  1594. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  1595. $expression = new AST\NewObjectExpression($className, $args);
  1596. // Defer NewObjectExpression validation
  1597. $this->deferredNewObjectExpressions[] = [
  1598. 'token' => $token,
  1599. 'expression' => $expression,
  1600. 'nestingLevel' => $this->nestingLevel,
  1601. ];
  1602. return $expression;
  1603. }
  1604. /**
  1605. * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
  1606. *
  1607. * @return mixed
  1608. */
  1609. public function NewObjectArg()
  1610. {
  1611. assert($this->lexer->lookahead !== null);
  1612. $token = $this->lexer->lookahead;
  1613. $peek = $this->lexer->glimpse();
  1614. assert($peek !== null);
  1615. if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
  1616. $this->match(TokenType::T_OPEN_PARENTHESIS);
  1617. $expression = $this->Subselect();
  1618. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  1619. return $expression;
  1620. }
  1621. return $this->ScalarExpression();
  1622. }
  1623. /**
  1624. * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression
  1625. *
  1626. * @return AST\IndexBy
  1627. */
  1628. public function IndexBy()
  1629. {
  1630. $this->match(TokenType::T_INDEX);
  1631. $this->match(TokenType::T_BY);
  1632. $pathExpr = $this->SingleValuedPathExpression();
  1633. // Add the INDEX BY info to the query component
  1634. $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
  1635. return new AST\IndexBy($pathExpr);
  1636. }
  1637. /**
  1638. * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary |
  1639. * StateFieldPathExpression | BooleanPrimary | CaseExpression |
  1640. * InstanceOfExpression
  1641. *
  1642. * @return mixed One of the possible expressions or subexpressions.
  1643. */
  1644. public function ScalarExpression()
  1645. {
  1646. assert($this->lexer->token !== null);
  1647. assert($this->lexer->lookahead !== null);
  1648. $lookahead = $this->lexer->lookahead->type;
  1649. $peek = $this->lexer->glimpse();
  1650. switch (true) {
  1651. case $lookahead === TokenType::T_INTEGER:
  1652. case $lookahead === TokenType::T_FLOAT:
  1653. // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 )
  1654. case $lookahead === TokenType::T_MINUS:
  1655. case $lookahead === TokenType::T_PLUS:
  1656. return $this->SimpleArithmeticExpression();
  1657. case $lookahead === TokenType::T_STRING:
  1658. return $this->StringPrimary();
  1659. case $lookahead === TokenType::T_TRUE:
  1660. case $lookahead === TokenType::T_FALSE:
  1661. $this->match($lookahead);
  1662. return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);
  1663. case $lookahead === TokenType::T_INPUT_PARAMETER:
  1664. switch (true) {
  1665. case $this->isMathOperator($peek):
  1666. // :param + u.value
  1667. return $this->SimpleArithmeticExpression();
  1668. default:
  1669. return $this->InputParameter();
  1670. }
  1671. case $lookahead === TokenType::T_CASE:
  1672. case $lookahead === TokenType::T_COALESCE:
  1673. case $lookahead === TokenType::T_NULLIF:
  1674. // Since NULLIF and COALESCE can be identified as a function,
  1675. // we need to check these before checking for FunctionDeclaration
  1676. return $this->CaseExpression();
  1677. case $lookahead === TokenType::T_OPEN_PARENTHESIS:
  1678. return $this->SimpleArithmeticExpression();
  1679. // this check must be done before checking for a filed path expression
  1680. case $this->isFunction():
  1681. $this->lexer->peek(); // "("
  1682. switch (true) {
  1683. case $this->isMathOperator($this->peekBeyondClosingParenthesis()):
  1684. // SUM(u.id) + COUNT(u.id)
  1685. return $this->SimpleArithmeticExpression();
  1686. default:
  1687. // IDENTITY(u)
  1688. return $this->FunctionDeclaration();
  1689. }
  1690. break;
  1691. // it is no function, so it must be a field path
  1692. case $lookahead === TokenType::T_IDENTIFIER:
  1693. $this->lexer->peek(); // lookahead => '.'
  1694. $this->lexer->peek(); // lookahead => token after '.'
  1695. $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
  1696. $this->lexer->resetPeek();
  1697. if ($this->isMathOperator($peek)) {
  1698. return $this->SimpleArithmeticExpression();
  1699. }
  1700. return $this->StateFieldPathExpression();
  1701. default:
  1702. $this->syntaxError();
  1703. }
  1704. }
  1705. /**
  1706. * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullIfExpression
  1707. * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
  1708. * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
  1709. * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
  1710. * CaseOperand ::= StateFieldPathExpression
  1711. * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
  1712. * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
  1713. * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
  1714. *
  1715. * @return mixed One of the possible expressions or subexpressions.
  1716. */
  1717. public function CaseExpression()
  1718. {
  1719. assert($this->lexer->lookahead !== null);
  1720. $lookahead = $this->lexer->lookahead->type;
  1721. switch ($lookahead) {
  1722. case TokenType::T_NULLIF:
  1723. return $this->NullIfExpression();
  1724. case TokenType::T_COALESCE:
  1725. return $this->CoalesceExpression();
  1726. case TokenType::T_CASE:
  1727. $this->lexer->resetPeek();
  1728. $peek = $this->lexer->peek();
  1729. assert($peek !== null);
  1730. if ($peek->type === TokenType::T_WHEN) {
  1731. return $this->GeneralCaseExpression();
  1732. }
  1733. return $this->SimpleCaseExpression();
  1734. default:
  1735. // Do nothing
  1736. break;
  1737. }
  1738. $this->syntaxError();
  1739. }
  1740. /**
  1741. * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
  1742. *
  1743. * @return AST\CoalesceExpression
  1744. */
  1745. public function CoalesceExpression()
  1746. {
  1747. $this->match(TokenType::T_COALESCE);
  1748. $this->match(TokenType::T_OPEN_PARENTHESIS);
  1749. // Process ScalarExpressions (1..N)
  1750. $scalarExpressions = [];
  1751. $scalarExpressions[] = $this->ScalarExpression();
  1752. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  1753. $this->match(TokenType::T_COMMA);
  1754. $scalarExpressions[] = $this->ScalarExpression();
  1755. }
  1756. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  1757. return new AST\CoalesceExpression($scalarExpressions);
  1758. }
  1759. /**
  1760. * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
  1761. *
  1762. * @return AST\NullIfExpression
  1763. */
  1764. public function NullIfExpression()
  1765. {
  1766. $this->match(TokenType::T_NULLIF);
  1767. $this->match(TokenType::T_OPEN_PARENTHESIS);
  1768. $firstExpression = $this->ScalarExpression();
  1769. $this->match(TokenType::T_COMMA);
  1770. $secondExpression = $this->ScalarExpression();
  1771. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  1772. return new AST\NullIfExpression($firstExpression, $secondExpression);
  1773. }
  1774. /**
  1775. * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
  1776. *
  1777. * @return AST\GeneralCaseExpression
  1778. */
  1779. public function GeneralCaseExpression()
  1780. {
  1781. $this->match(TokenType::T_CASE);
  1782. // Process WhenClause (1..N)
  1783. $whenClauses = [];
  1784. do {
  1785. $whenClauses[] = $this->WhenClause();
  1786. } while ($this->lexer->isNextToken(TokenType::T_WHEN));
  1787. $this->match(TokenType::T_ELSE);
  1788. $scalarExpression = $this->ScalarExpression();
  1789. $this->match(TokenType::T_END);
  1790. return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
  1791. }
  1792. /**
  1793. * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
  1794. * CaseOperand ::= StateFieldPathExpression
  1795. *
  1796. * @return AST\SimpleCaseExpression
  1797. */
  1798. public function SimpleCaseExpression()
  1799. {
  1800. $this->match(TokenType::T_CASE);
  1801. $caseOperand = $this->StateFieldPathExpression();
  1802. // Process SimpleWhenClause (1..N)
  1803. $simpleWhenClauses = [];
  1804. do {
  1805. $simpleWhenClauses[] = $this->SimpleWhenClause();
  1806. } while ($this->lexer->isNextToken(TokenType::T_WHEN));
  1807. $this->match(TokenType::T_ELSE);
  1808. $scalarExpression = $this->ScalarExpression();
  1809. $this->match(TokenType::T_END);
  1810. return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
  1811. }
  1812. /**
  1813. * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
  1814. *
  1815. * @return AST\WhenClause
  1816. */
  1817. public function WhenClause()
  1818. {
  1819. $this->match(TokenType::T_WHEN);
  1820. $conditionalExpression = $this->ConditionalExpression();
  1821. $this->match(TokenType::T_THEN);
  1822. return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
  1823. }
  1824. /**
  1825. * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
  1826. *
  1827. * @return AST\SimpleWhenClause
  1828. */
  1829. public function SimpleWhenClause()
  1830. {
  1831. $this->match(TokenType::T_WHEN);
  1832. $conditionalExpression = $this->ScalarExpression();
  1833. $this->match(TokenType::T_THEN);
  1834. return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
  1835. }
  1836. /**
  1837. * SelectExpression ::= (
  1838. * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
  1839. * PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
  1840. * ) [["AS"] ["HIDDEN"] AliasResultVariable]
  1841. *
  1842. * @return AST\SelectExpression
  1843. */
  1844. public function SelectExpression()
  1845. {
  1846. assert($this->lexer->lookahead !== null);
  1847. $expression = null;
  1848. $identVariable = null;
  1849. $peek = $this->lexer->glimpse();
  1850. $lookaheadType = $this->lexer->lookahead->type;
  1851. assert($peek !== null);
  1852. switch (true) {
  1853. // ScalarExpression (u.name)
  1854. case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT:
  1855. $expression = $this->ScalarExpression();
  1856. break;
  1857. // IdentificationVariable (u)
  1858. case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS:
  1859. $expression = $identVariable = $this->IdentificationVariable();
  1860. break;
  1861. // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
  1862. case $lookaheadType === TokenType::T_CASE:
  1863. case $lookaheadType === TokenType::T_COALESCE:
  1864. case $lookaheadType === TokenType::T_NULLIF:
  1865. $expression = $this->CaseExpression();
  1866. break;
  1867. // DQL Function (SUM(u.value) or SUM(u.value) + 1)
  1868. case $this->isFunction():
  1869. $this->lexer->peek(); // "("
  1870. switch (true) {
  1871. case $this->isMathOperator($this->peekBeyondClosingParenthesis()):
  1872. // SUM(u.id) + COUNT(u.id)
  1873. $expression = $this->ScalarExpression();
  1874. break;
  1875. default:
  1876. // IDENTITY(u)
  1877. $expression = $this->FunctionDeclaration();
  1878. break;
  1879. }
  1880. break;
  1881. // PartialObjectExpression (PARTIAL u.{id, name})
  1882. case $lookaheadType === TokenType::T_PARTIAL:
  1883. $expression = $this->PartialObjectExpression();
  1884. $identVariable = $expression->identificationVariable;
  1885. break;
  1886. // Subselect
  1887. case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT:
  1888. $this->match(TokenType::T_OPEN_PARENTHESIS);
  1889. $expression = $this->Subselect();
  1890. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  1891. break;
  1892. // Shortcut: ScalarExpression => SimpleArithmeticExpression
  1893. case $lookaheadType === TokenType::T_OPEN_PARENTHESIS:
  1894. case $lookaheadType === TokenType::T_INTEGER:
  1895. case $lookaheadType === TokenType::T_STRING:
  1896. case $lookaheadType === TokenType::T_FLOAT:
  1897. // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
  1898. case $lookaheadType === TokenType::T_MINUS:
  1899. case $lookaheadType === TokenType::T_PLUS:
  1900. $expression = $this->SimpleArithmeticExpression();
  1901. break;
  1902. // NewObjectExpression (New ClassName(id, name))
  1903. case $lookaheadType === TokenType::T_NEW:
  1904. $expression = $this->NewObjectExpression();
  1905. break;
  1906. default:
  1907. $this->syntaxError(
  1908. 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
  1909. $this->lexer->lookahead
  1910. );
  1911. }
  1912. // [["AS"] ["HIDDEN"] AliasResultVariable]
  1913. $mustHaveAliasResultVariable = false;
  1914. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1915. $this->match(TokenType::T_AS);
  1916. $mustHaveAliasResultVariable = true;
  1917. }
  1918. $hiddenAliasResultVariable = false;
  1919. if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) {
  1920. $this->match(TokenType::T_HIDDEN);
  1921. $hiddenAliasResultVariable = true;
  1922. }
  1923. $aliasResultVariable = null;
  1924. if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
  1925. assert($expression instanceof AST\Node || is_string($expression));
  1926. $token = $this->lexer->lookahead;
  1927. $aliasResultVariable = $this->AliasResultVariable();
  1928. // Include AliasResultVariable in query components.
  1929. $this->queryComponents[$aliasResultVariable] = [
  1930. 'resultVariable' => $expression,
  1931. 'nestingLevel' => $this->nestingLevel,
  1932. 'token' => $token,
  1933. ];
  1934. }
  1935. // AST
  1936. $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
  1937. if ($identVariable) {
  1938. $this->identVariableExpressions[$identVariable] = $expr;
  1939. }
  1940. return $expr;
  1941. }
  1942. /**
  1943. * SimpleSelectExpression ::= (
  1944. * StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
  1945. * AggregateExpression | "(" Subselect ")" | ScalarExpression
  1946. * ) [["AS"] AliasResultVariable]
  1947. *
  1948. * @return AST\SimpleSelectExpression
  1949. */
  1950. public function SimpleSelectExpression()
  1951. {
  1952. assert($this->lexer->lookahead !== null);
  1953. $peek = $this->lexer->glimpse();
  1954. assert($peek !== null);
  1955. switch ($this->lexer->lookahead->type) {
  1956. case TokenType::T_IDENTIFIER:
  1957. switch (true) {
  1958. case $peek->type === TokenType::T_DOT:
  1959. $expression = $this->StateFieldPathExpression();
  1960. return new AST\SimpleSelectExpression($expression);
  1961. case $peek->type !== TokenType::T_OPEN_PARENTHESIS:
  1962. $expression = $this->IdentificationVariable();
  1963. return new AST\SimpleSelectExpression($expression);
  1964. case $this->isFunction():
  1965. // SUM(u.id) + COUNT(u.id)
  1966. if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {
  1967. return new AST\SimpleSelectExpression($this->ScalarExpression());
  1968. }
  1969. // COUNT(u.id)
  1970. if ($this->isAggregateFunction($this->lexer->lookahead->type)) {
  1971. return new AST\SimpleSelectExpression($this->AggregateExpression());
  1972. }
  1973. // IDENTITY(u)
  1974. return new AST\SimpleSelectExpression($this->FunctionDeclaration());
  1975. default:
  1976. // Do nothing
  1977. }
  1978. break;
  1979. case TokenType::T_OPEN_PARENTHESIS:
  1980. if ($peek->type !== TokenType::T_SELECT) {
  1981. // Shortcut: ScalarExpression => SimpleArithmeticExpression
  1982. $expression = $this->SimpleArithmeticExpression();
  1983. return new AST\SimpleSelectExpression($expression);
  1984. }
  1985. // Subselect
  1986. $this->match(TokenType::T_OPEN_PARENTHESIS);
  1987. $expression = $this->Subselect();
  1988. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  1989. return new AST\SimpleSelectExpression($expression);
  1990. default:
  1991. // Do nothing
  1992. }
  1993. $this->lexer->peek();
  1994. $expression = $this->ScalarExpression();
  1995. $expr = new AST\SimpleSelectExpression($expression);
  1996. if ($this->lexer->isNextToken(TokenType::T_AS)) {
  1997. $this->match(TokenType::T_AS);
  1998. }
  1999. if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
  2000. $token = $this->lexer->lookahead;
  2001. $resultVariable = $this->AliasResultVariable();
  2002. $expr->fieldIdentificationVariable = $resultVariable;
  2003. // Include AliasResultVariable in query components.
  2004. $this->queryComponents[$resultVariable] = [
  2005. 'resultvariable' => $expr,
  2006. 'nestingLevel' => $this->nestingLevel,
  2007. 'token' => $token,
  2008. ];
  2009. }
  2010. return $expr;
  2011. }
  2012. /**
  2013. * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
  2014. *
  2015. * @return AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm
  2016. */
  2017. public function ConditionalExpression()
  2018. {
  2019. $conditionalTerms = [];
  2020. $conditionalTerms[] = $this->ConditionalTerm();
  2021. while ($this->lexer->isNextToken(TokenType::T_OR)) {
  2022. $this->match(TokenType::T_OR);
  2023. $conditionalTerms[] = $this->ConditionalTerm();
  2024. }
  2025. // Phase 1 AST optimization: Prevent AST\ConditionalExpression
  2026. // if only one AST\ConditionalTerm is defined
  2027. if (count($conditionalTerms) === 1) {
  2028. return $conditionalTerms[0];
  2029. }
  2030. return new AST\ConditionalExpression($conditionalTerms);
  2031. }
  2032. /**
  2033. * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
  2034. *
  2035. * @return AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm
  2036. */
  2037. public function ConditionalTerm()
  2038. {
  2039. $conditionalFactors = [];
  2040. $conditionalFactors[] = $this->ConditionalFactor();
  2041. while ($this->lexer->isNextToken(TokenType::T_AND)) {
  2042. $this->match(TokenType::T_AND);
  2043. $conditionalFactors[] = $this->ConditionalFactor();
  2044. }
  2045. // Phase 1 AST optimization: Prevent AST\ConditionalTerm
  2046. // if only one AST\ConditionalFactor is defined
  2047. if (count($conditionalFactors) === 1) {
  2048. return $conditionalFactors[0];
  2049. }
  2050. return new AST\ConditionalTerm($conditionalFactors);
  2051. }
  2052. /**
  2053. * ConditionalFactor ::= ["NOT"] ConditionalPrimary
  2054. *
  2055. * @return AST\ConditionalFactor|AST\ConditionalPrimary
  2056. */
  2057. public function ConditionalFactor()
  2058. {
  2059. $not = false;
  2060. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2061. $this->match(TokenType::T_NOT);
  2062. $not = true;
  2063. }
  2064. $conditionalPrimary = $this->ConditionalPrimary();
  2065. // Phase 1 AST optimization: Prevent AST\ConditionalFactor
  2066. // if only one AST\ConditionalPrimary is defined
  2067. if (! $not) {
  2068. return $conditionalPrimary;
  2069. }
  2070. return new AST\ConditionalFactor($conditionalPrimary, $not);
  2071. }
  2072. /**
  2073. * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
  2074. *
  2075. * @return AST\ConditionalPrimary
  2076. */
  2077. public function ConditionalPrimary()
  2078. {
  2079. $condPrimary = new AST\ConditionalPrimary();
  2080. if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {
  2081. $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
  2082. return $condPrimary;
  2083. }
  2084. // Peek beyond the matching closing parenthesis ')'
  2085. $peek = $this->peekBeyondClosingParenthesis();
  2086. if (
  2087. $peek !== null && (
  2088. in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) ||
  2089. in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) ||
  2090. $this->isMathOperator($peek)
  2091. )
  2092. ) {
  2093. $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
  2094. return $condPrimary;
  2095. }
  2096. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2097. $condPrimary->conditionalExpression = $this->ConditionalExpression();
  2098. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2099. return $condPrimary;
  2100. }
  2101. /**
  2102. * SimpleConditionalExpression ::=
  2103. * ComparisonExpression | BetweenExpression | LikeExpression |
  2104. * InExpression | NullComparisonExpression | ExistsExpression |
  2105. * EmptyCollectionComparisonExpression | CollectionMemberExpression |
  2106. * InstanceOfExpression
  2107. *
  2108. * @return (AST\BetweenExpression|
  2109. * AST\CollectionMemberExpression|
  2110. * AST\ComparisonExpression|
  2111. * AST\EmptyCollectionComparisonExpression|
  2112. * AST\ExistsExpression|
  2113. * AST\InExpression|
  2114. * AST\InstanceOfExpression|
  2115. * AST\LikeExpression|
  2116. * AST\NullComparisonExpression)
  2117. *
  2118. * @phpstan-ignore return.deprecatedClass
  2119. */
  2120. public function SimpleConditionalExpression()
  2121. {
  2122. assert($this->lexer->lookahead !== null);
  2123. if ($this->lexer->isNextToken(TokenType::T_EXISTS)) {
  2124. return $this->ExistsExpression();
  2125. }
  2126. $token = $this->lexer->lookahead;
  2127. $peek = $this->lexer->glimpse();
  2128. $lookahead = $token;
  2129. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2130. $token = $this->lexer->glimpse();
  2131. }
  2132. assert($token !== null);
  2133. assert($peek !== null);
  2134. if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) {
  2135. // Peek beyond the matching closing parenthesis.
  2136. $beyond = $this->lexer->peek();
  2137. switch ($peek->value) {
  2138. case '(':
  2139. // Peeks beyond the matched closing parenthesis.
  2140. $token = $this->peekBeyondClosingParenthesis(false);
  2141. assert($token !== null);
  2142. if ($token->type === TokenType::T_NOT) {
  2143. $token = $this->lexer->peek();
  2144. assert($token !== null);
  2145. }
  2146. if ($token->type === TokenType::T_IS) {
  2147. $lookahead = $this->lexer->peek();
  2148. }
  2149. break;
  2150. default:
  2151. // Peek beyond the PathExpression or InputParameter.
  2152. $token = $beyond;
  2153. while ($token->value === '.') {
  2154. $this->lexer->peek();
  2155. $token = $this->lexer->peek();
  2156. assert($token !== null);
  2157. }
  2158. // Also peek beyond a NOT if there is one.
  2159. assert($token !== null);
  2160. if ($token->type === TokenType::T_NOT) {
  2161. $token = $this->lexer->peek();
  2162. assert($token !== null);
  2163. }
  2164. // We need to go even further in case of IS (differentiate between NULL and EMPTY)
  2165. $lookahead = $this->lexer->peek();
  2166. }
  2167. assert($lookahead !== null);
  2168. // Also peek beyond a NOT if there is one.
  2169. if ($lookahead->type === TokenType::T_NOT) {
  2170. $lookahead = $this->lexer->peek();
  2171. }
  2172. $this->lexer->resetPeek();
  2173. }
  2174. if ($token->type === TokenType::T_BETWEEN) {
  2175. return $this->BetweenExpression();
  2176. }
  2177. if ($token->type === TokenType::T_LIKE) {
  2178. return $this->LikeExpression();
  2179. }
  2180. if ($token->type === TokenType::T_IN) {
  2181. return $this->InExpression();
  2182. }
  2183. if ($token->type === TokenType::T_INSTANCE) {
  2184. return $this->InstanceOfExpression();
  2185. }
  2186. if ($token->type === TokenType::T_MEMBER) {
  2187. return $this->CollectionMemberExpression();
  2188. }
  2189. assert($lookahead !== null);
  2190. if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) {
  2191. return $this->NullComparisonExpression();
  2192. }
  2193. if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) {
  2194. return $this->EmptyCollectionComparisonExpression();
  2195. }
  2196. return $this->ComparisonExpression();
  2197. }
  2198. /**
  2199. * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
  2200. *
  2201. * @return AST\EmptyCollectionComparisonExpression
  2202. */
  2203. public function EmptyCollectionComparisonExpression()
  2204. {
  2205. $pathExpression = $this->CollectionValuedPathExpression();
  2206. $this->match(TokenType::T_IS);
  2207. $not = false;
  2208. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2209. $this->match(TokenType::T_NOT);
  2210. $not = true;
  2211. }
  2212. $this->match(TokenType::T_EMPTY);
  2213. return new AST\EmptyCollectionComparisonExpression(
  2214. $pathExpression,
  2215. $not
  2216. );
  2217. }
  2218. /**
  2219. * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
  2220. *
  2221. * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
  2222. * SimpleEntityExpression ::= IdentificationVariable | InputParameter
  2223. *
  2224. * @return AST\CollectionMemberExpression
  2225. */
  2226. public function CollectionMemberExpression()
  2227. {
  2228. $not = false;
  2229. $entityExpr = $this->EntityExpression();
  2230. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2231. $this->match(TokenType::T_NOT);
  2232. $not = true;
  2233. }
  2234. $this->match(TokenType::T_MEMBER);
  2235. if ($this->lexer->isNextToken(TokenType::T_OF)) {
  2236. $this->match(TokenType::T_OF);
  2237. }
  2238. return new AST\CollectionMemberExpression(
  2239. $entityExpr,
  2240. $this->CollectionValuedPathExpression(),
  2241. $not
  2242. );
  2243. }
  2244. /**
  2245. * Literal ::= string | char | integer | float | boolean
  2246. *
  2247. * @return AST\Literal
  2248. */
  2249. public function Literal()
  2250. {
  2251. assert($this->lexer->lookahead !== null);
  2252. assert($this->lexer->token !== null);
  2253. switch ($this->lexer->lookahead->type) {
  2254. case TokenType::T_STRING:
  2255. $this->match(TokenType::T_STRING);
  2256. return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);
  2257. case TokenType::T_INTEGER:
  2258. case TokenType::T_FLOAT:
  2259. $this->match(
  2260. $this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT
  2261. );
  2262. return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value);
  2263. case TokenType::T_TRUE:
  2264. case TokenType::T_FALSE:
  2265. $this->match(
  2266. $this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE
  2267. );
  2268. return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);
  2269. default:
  2270. $this->syntaxError('Literal');
  2271. }
  2272. }
  2273. /**
  2274. * InParameter ::= ArithmeticExpression | InputParameter
  2275. *
  2276. * @return AST\InputParameter|AST\ArithmeticExpression
  2277. */
  2278. public function InParameter()
  2279. {
  2280. assert($this->lexer->lookahead !== null);
  2281. if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) {
  2282. return $this->InputParameter();
  2283. }
  2284. return $this->ArithmeticExpression();
  2285. }
  2286. /**
  2287. * InputParameter ::= PositionalParameter | NamedParameter
  2288. *
  2289. * @return AST\InputParameter
  2290. */
  2291. public function InputParameter()
  2292. {
  2293. $this->match(TokenType::T_INPUT_PARAMETER);
  2294. assert($this->lexer->token !== null);
  2295. return new AST\InputParameter($this->lexer->token->value);
  2296. }
  2297. /**
  2298. * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
  2299. *
  2300. * @return AST\ArithmeticExpression
  2301. */
  2302. public function ArithmeticExpression()
  2303. {
  2304. $expr = new AST\ArithmeticExpression();
  2305. if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {
  2306. $peek = $this->lexer->glimpse();
  2307. assert($peek !== null);
  2308. if ($peek->type === TokenType::T_SELECT) {
  2309. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2310. $expr->subselect = $this->Subselect();
  2311. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2312. return $expr;
  2313. }
  2314. }
  2315. $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
  2316. return $expr;
  2317. }
  2318. /**
  2319. * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
  2320. *
  2321. * @return AST\SimpleArithmeticExpression|AST\ArithmeticTerm
  2322. */
  2323. public function SimpleArithmeticExpression()
  2324. {
  2325. $terms = [];
  2326. $terms[] = $this->ArithmeticTerm();
  2327. while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) {
  2328. $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS);
  2329. assert($this->lexer->token !== null);
  2330. $terms[] = $this->lexer->token->value;
  2331. $terms[] = $this->ArithmeticTerm();
  2332. }
  2333. // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
  2334. // if only one AST\ArithmeticTerm is defined
  2335. if (count($terms) === 1) {
  2336. return $terms[0];
  2337. }
  2338. return new AST\SimpleArithmeticExpression($terms);
  2339. }
  2340. /**
  2341. * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
  2342. *
  2343. * @return AST\ArithmeticTerm
  2344. */
  2345. public function ArithmeticTerm()
  2346. {
  2347. $factors = [];
  2348. $factors[] = $this->ArithmeticFactor();
  2349. while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) {
  2350. $this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE);
  2351. assert($this->lexer->token !== null);
  2352. $factors[] = $this->lexer->token->value;
  2353. $factors[] = $this->ArithmeticFactor();
  2354. }
  2355. // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
  2356. // if only one AST\ArithmeticFactor is defined
  2357. if (count($factors) === 1) {
  2358. return $factors[0];
  2359. }
  2360. return new AST\ArithmeticTerm($factors);
  2361. }
  2362. /**
  2363. * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
  2364. *
  2365. * @return AST\ArithmeticFactor
  2366. */
  2367. public function ArithmeticFactor()
  2368. {
  2369. $sign = null;
  2370. $isPlus = $this->lexer->isNextToken(TokenType::T_PLUS);
  2371. if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) {
  2372. $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS);
  2373. $sign = $isPlus;
  2374. }
  2375. $primary = $this->ArithmeticPrimary();
  2376. // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
  2377. // if only one AST\ArithmeticPrimary is defined
  2378. if ($sign === null) {
  2379. return $primary;
  2380. }
  2381. return new AST\ArithmeticFactor($primary, $sign);
  2382. }
  2383. /**
  2384. * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
  2385. * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
  2386. * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
  2387. * | InputParameter | CaseExpression
  2388. *
  2389. * @return AST\Node|string
  2390. */
  2391. public function ArithmeticPrimary()
  2392. {
  2393. if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {
  2394. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2395. $expr = $this->SimpleArithmeticExpression();
  2396. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2397. return new AST\ParenthesisExpression($expr);
  2398. }
  2399. if ($this->lexer->lookahead === null) {
  2400. $this->syntaxError('ArithmeticPrimary');
  2401. }
  2402. switch ($this->lexer->lookahead->type) {
  2403. case TokenType::T_COALESCE:
  2404. case TokenType::T_NULLIF:
  2405. case TokenType::T_CASE:
  2406. return $this->CaseExpression();
  2407. case TokenType::T_IDENTIFIER:
  2408. $peek = $this->lexer->glimpse();
  2409. if ($peek !== null && $peek->value === '(') {
  2410. return $this->FunctionDeclaration();
  2411. }
  2412. if ($peek !== null && $peek->value === '.') {
  2413. return $this->SingleValuedPathExpression();
  2414. }
  2415. if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) {
  2416. return $this->ResultVariable();
  2417. }
  2418. return $this->StateFieldPathExpression();
  2419. case TokenType::T_INPUT_PARAMETER:
  2420. return $this->InputParameter();
  2421. default:
  2422. $peek = $this->lexer->glimpse();
  2423. if ($peek !== null && $peek->value === '(') {
  2424. return $this->FunctionDeclaration();
  2425. }
  2426. return $this->Literal();
  2427. }
  2428. }
  2429. /**
  2430. * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
  2431. *
  2432. * @return AST\Subselect|AST\Node|string
  2433. */
  2434. public function StringExpression()
  2435. {
  2436. $peek = $this->lexer->glimpse();
  2437. assert($peek !== null);
  2438. // Subselect
  2439. if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) {
  2440. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2441. $expr = $this->Subselect();
  2442. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2443. return $expr;
  2444. }
  2445. assert($this->lexer->lookahead !== null);
  2446. // ResultVariable (string)
  2447. if (
  2448. $this->lexer->isNextToken(TokenType::T_IDENTIFIER) &&
  2449. isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])
  2450. ) {
  2451. return $this->ResultVariable();
  2452. }
  2453. return $this->StringPrimary();
  2454. }
  2455. /**
  2456. * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
  2457. *
  2458. * @return AST\Node
  2459. */
  2460. public function StringPrimary()
  2461. {
  2462. assert($this->lexer->lookahead !== null);
  2463. $lookaheadType = $this->lexer->lookahead->type;
  2464. switch ($lookaheadType) {
  2465. case TokenType::T_IDENTIFIER:
  2466. $peek = $this->lexer->glimpse();
  2467. assert($peek !== null);
  2468. if ($peek->value === '.') {
  2469. return $this->StateFieldPathExpression();
  2470. }
  2471. if ($peek->value === '(') {
  2472. // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
  2473. return $this->FunctionDeclaration();
  2474. }
  2475. $this->syntaxError("'.' or '('");
  2476. break;
  2477. case TokenType::T_STRING:
  2478. $this->match(TokenType::T_STRING);
  2479. assert($this->lexer->token !== null);
  2480. return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);
  2481. case TokenType::T_INPUT_PARAMETER:
  2482. return $this->InputParameter();
  2483. case TokenType::T_CASE:
  2484. case TokenType::T_COALESCE:
  2485. case TokenType::T_NULLIF:
  2486. return $this->CaseExpression();
  2487. default:
  2488. assert($lookaheadType !== null);
  2489. if ($this->isAggregateFunction($lookaheadType)) {
  2490. return $this->AggregateExpression();
  2491. }
  2492. }
  2493. $this->syntaxError(
  2494. 'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
  2495. );
  2496. }
  2497. /**
  2498. * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
  2499. *
  2500. * @return AST\InputParameter|AST\PathExpression
  2501. */
  2502. public function EntityExpression()
  2503. {
  2504. $glimpse = $this->lexer->glimpse();
  2505. assert($glimpse !== null);
  2506. if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') {
  2507. return $this->SingleValuedAssociationPathExpression();
  2508. }
  2509. return $this->SimpleEntityExpression();
  2510. }
  2511. /**
  2512. * SimpleEntityExpression ::= IdentificationVariable | InputParameter
  2513. *
  2514. * @return AST\InputParameter|AST\PathExpression
  2515. */
  2516. public function SimpleEntityExpression()
  2517. {
  2518. if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
  2519. return $this->InputParameter();
  2520. }
  2521. return $this->StateFieldPathExpression();
  2522. }
  2523. /**
  2524. * AggregateExpression ::=
  2525. * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
  2526. *
  2527. * @return AST\AggregateExpression
  2528. */
  2529. public function AggregateExpression()
  2530. {
  2531. assert($this->lexer->lookahead !== null);
  2532. $lookaheadType = $this->lexer->lookahead->type;
  2533. $isDistinct = false;
  2534. if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) {
  2535. $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
  2536. }
  2537. $this->match($lookaheadType);
  2538. assert($this->lexer->token !== null);
  2539. $functionName = $this->lexer->token->value;
  2540. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2541. if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {
  2542. $this->match(TokenType::T_DISTINCT);
  2543. $isDistinct = true;
  2544. }
  2545. $pathExp = $this->SimpleArithmeticExpression();
  2546. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2547. return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
  2548. }
  2549. /**
  2550. * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
  2551. *
  2552. * @return AST\QuantifiedExpression
  2553. */
  2554. public function QuantifiedExpression()
  2555. {
  2556. assert($this->lexer->lookahead !== null);
  2557. $lookaheadType = $this->lexer->lookahead->type;
  2558. $value = $this->lexer->lookahead->value;
  2559. if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) {
  2560. $this->syntaxError('ALL, ANY or SOME');
  2561. }
  2562. $this->match($lookaheadType);
  2563. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2564. $qExpr = new AST\QuantifiedExpression($this->Subselect());
  2565. $qExpr->type = $value;
  2566. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2567. return $qExpr;
  2568. }
  2569. /**
  2570. * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
  2571. *
  2572. * @return AST\BetweenExpression
  2573. */
  2574. public function BetweenExpression()
  2575. {
  2576. $not = false;
  2577. $arithExpr1 = $this->ArithmeticExpression();
  2578. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2579. $this->match(TokenType::T_NOT);
  2580. $not = true;
  2581. }
  2582. $this->match(TokenType::T_BETWEEN);
  2583. $arithExpr2 = $this->ArithmeticExpression();
  2584. $this->match(TokenType::T_AND);
  2585. $arithExpr3 = $this->ArithmeticExpression();
  2586. return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not);
  2587. }
  2588. /**
  2589. * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
  2590. *
  2591. * @return AST\ComparisonExpression
  2592. */
  2593. public function ComparisonExpression()
  2594. {
  2595. $this->lexer->glimpse();
  2596. $leftExpr = $this->ArithmeticExpression();
  2597. $operator = $this->ComparisonOperator();
  2598. $rightExpr = $this->isNextAllAnySome()
  2599. ? $this->QuantifiedExpression()
  2600. : $this->ArithmeticExpression();
  2601. return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
  2602. }
  2603. /**
  2604. * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
  2605. *
  2606. * @return AST\InListExpression|AST\InSubselectExpression
  2607. */
  2608. public function InExpression()
  2609. {
  2610. $expression = $this->ArithmeticExpression();
  2611. $not = false;
  2612. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2613. $this->match(TokenType::T_NOT);
  2614. $not = true;
  2615. }
  2616. $this->match(TokenType::T_IN);
  2617. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2618. if ($this->lexer->isNextToken(TokenType::T_SELECT)) {
  2619. $inExpression = new AST\InSubselectExpression(
  2620. $expression,
  2621. $this->Subselect(),
  2622. $not
  2623. );
  2624. } else {
  2625. $literals = [$this->InParameter()];
  2626. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  2627. $this->match(TokenType::T_COMMA);
  2628. $literals[] = $this->InParameter();
  2629. }
  2630. $inExpression = new AST\InListExpression(
  2631. $expression,
  2632. $literals,
  2633. $not
  2634. );
  2635. }
  2636. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2637. return $inExpression;
  2638. }
  2639. /**
  2640. * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
  2641. *
  2642. * @return AST\InstanceOfExpression
  2643. */
  2644. public function InstanceOfExpression()
  2645. {
  2646. $identificationVariable = $this->IdentificationVariable();
  2647. $not = false;
  2648. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2649. $this->match(TokenType::T_NOT);
  2650. $not = true;
  2651. }
  2652. $this->match(TokenType::T_INSTANCE);
  2653. $this->match(TokenType::T_OF);
  2654. $exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)
  2655. ? $this->InstanceOfParameterList()
  2656. : [$this->InstanceOfParameter()];
  2657. return new AST\InstanceOfExpression(
  2658. $identificationVariable,
  2659. $exprValues,
  2660. $not
  2661. );
  2662. }
  2663. /** @return non-empty-list<AST\InputParameter|string> */
  2664. public function InstanceOfParameterList(): array
  2665. {
  2666. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2667. $exprValues = [$this->InstanceOfParameter()];
  2668. while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
  2669. $this->match(TokenType::T_COMMA);
  2670. $exprValues[] = $this->InstanceOfParameter();
  2671. }
  2672. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2673. return $exprValues;
  2674. }
  2675. /**
  2676. * InstanceOfParameter ::= AbstractSchemaName | InputParameter
  2677. *
  2678. * @return AST\InputParameter|string
  2679. */
  2680. public function InstanceOfParameter()
  2681. {
  2682. if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
  2683. $this->match(TokenType::T_INPUT_PARAMETER);
  2684. assert($this->lexer->token !== null);
  2685. return new AST\InputParameter($this->lexer->token->value);
  2686. }
  2687. $abstractSchemaName = $this->AbstractSchemaName();
  2688. $this->validateAbstractSchemaName($abstractSchemaName);
  2689. return $abstractSchemaName;
  2690. }
  2691. /**
  2692. * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
  2693. *
  2694. * @return AST\LikeExpression
  2695. */
  2696. public function LikeExpression()
  2697. {
  2698. $stringExpr = $this->StringExpression();
  2699. $not = false;
  2700. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2701. $this->match(TokenType::T_NOT);
  2702. $not = true;
  2703. }
  2704. $this->match(TokenType::T_LIKE);
  2705. if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
  2706. $this->match(TokenType::T_INPUT_PARAMETER);
  2707. assert($this->lexer->token !== null);
  2708. $stringPattern = new AST\InputParameter($this->lexer->token->value);
  2709. } else {
  2710. $stringPattern = $this->StringPrimary();
  2711. }
  2712. $escapeChar = null;
  2713. if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) {
  2714. $this->match(TokenType::T_ESCAPE);
  2715. $this->match(TokenType::T_STRING);
  2716. assert($this->lexer->token !== null);
  2717. $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);
  2718. }
  2719. return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not);
  2720. }
  2721. /**
  2722. * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
  2723. *
  2724. * @return AST\NullComparisonExpression
  2725. */
  2726. public function NullComparisonExpression()
  2727. {
  2728. switch (true) {
  2729. case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER):
  2730. $this->match(TokenType::T_INPUT_PARAMETER);
  2731. assert($this->lexer->token !== null);
  2732. $expr = new AST\InputParameter($this->lexer->token->value);
  2733. break;
  2734. case $this->lexer->isNextToken(TokenType::T_NULLIF):
  2735. $expr = $this->NullIfExpression();
  2736. break;
  2737. case $this->lexer->isNextToken(TokenType::T_COALESCE):
  2738. $expr = $this->CoalesceExpression();
  2739. break;
  2740. case $this->isFunction():
  2741. $expr = $this->FunctionDeclaration();
  2742. break;
  2743. default:
  2744. // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
  2745. $glimpse = $this->lexer->glimpse();
  2746. assert($glimpse !== null);
  2747. if ($glimpse->type === TokenType::T_DOT) {
  2748. $expr = $this->SingleValuedPathExpression();
  2749. // Leave switch statement
  2750. break;
  2751. }
  2752. assert($this->lexer->lookahead !== null);
  2753. $lookaheadValue = $this->lexer->lookahead->value;
  2754. // Validate existing component
  2755. if (! isset($this->queryComponents[$lookaheadValue])) {
  2756. $this->semanticalError('Cannot add having condition on undefined result variable.');
  2757. }
  2758. // Validate SingleValuedPathExpression (ie.: "product")
  2759. if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
  2760. $expr = $this->SingleValuedPathExpression();
  2761. break;
  2762. }
  2763. // Validating ResultVariable
  2764. if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
  2765. $this->semanticalError('Cannot add having condition on a non result variable.');
  2766. }
  2767. $expr = $this->ResultVariable();
  2768. break;
  2769. }
  2770. $this->match(TokenType::T_IS);
  2771. $not = false;
  2772. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2773. $this->match(TokenType::T_NOT);
  2774. $not = true;
  2775. }
  2776. $this->match(TokenType::T_NULL);
  2777. return new AST\NullComparisonExpression($expr, $not);
  2778. }
  2779. /**
  2780. * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
  2781. *
  2782. * @return AST\ExistsExpression
  2783. */
  2784. public function ExistsExpression()
  2785. {
  2786. $not = false;
  2787. if ($this->lexer->isNextToken(TokenType::T_NOT)) {
  2788. $this->match(TokenType::T_NOT);
  2789. $not = true;
  2790. }
  2791. $this->match(TokenType::T_EXISTS);
  2792. $this->match(TokenType::T_OPEN_PARENTHESIS);
  2793. $subselect = $this->Subselect();
  2794. $this->match(TokenType::T_CLOSE_PARENTHESIS);
  2795. return new AST\ExistsExpression($subselect, $not);
  2796. }
  2797. /**
  2798. * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
  2799. *
  2800. * @return string
  2801. */
  2802. public function ComparisonOperator()
  2803. {
  2804. assert($this->lexer->lookahead !== null);
  2805. switch ($this->lexer->lookahead->value) {
  2806. case '=':
  2807. $this->match(TokenType::T_EQUALS);
  2808. return '=';
  2809. case '<':
  2810. $this->match(TokenType::T_LOWER_THAN);
  2811. $operator = '<';
  2812. if ($this->lexer->isNextToken(TokenType::T_EQUALS)) {
  2813. $this->match(TokenType::T_EQUALS);
  2814. $operator .= '=';
  2815. } elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) {
  2816. $this->match(TokenType::T_GREATER_THAN);
  2817. $operator .= '>';
  2818. }
  2819. return $operator;
  2820. case '>':
  2821. $this->match(TokenType::T_GREATER_THAN);
  2822. $operator = '>';
  2823. if ($this->lexer->isNextToken(TokenType::T_EQUALS)) {
  2824. $this->match(TokenType::T_EQUALS);
  2825. $operator .= '=';
  2826. }
  2827. return $operator;
  2828. case '!':
  2829. $this->match(TokenType::T_NEGATE);
  2830. $this->match(TokenType::T_EQUALS);
  2831. return '<>';
  2832. default:
  2833. $this->syntaxError('=, <, <=, <>, >, >=, !=');
  2834. }
  2835. }
  2836. /**
  2837. * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
  2838. *
  2839. * @return Functions\FunctionNode
  2840. */
  2841. public function FunctionDeclaration()
  2842. {
  2843. assert($this->lexer->lookahead !== null);
  2844. $token = $this->lexer->lookahead;
  2845. $funcName = strtolower($token->value);
  2846. $customFunctionDeclaration = $this->CustomFunctionDeclaration();
  2847. // Check for custom functions functions first!
  2848. switch (true) {
  2849. case $customFunctionDeclaration !== null:
  2850. return $customFunctionDeclaration;
  2851. case isset(self::$stringFunctions[$funcName]):
  2852. return $this->FunctionsReturningStrings();
  2853. case isset(self::$numericFunctions[$funcName]):
  2854. return $this->FunctionsReturningNumerics();
  2855. case isset(self::$datetimeFunctions[$funcName]):
  2856. return $this->FunctionsReturningDatetime();
  2857. default:
  2858. $this->syntaxError('known function', $token);
  2859. }
  2860. }
  2861. /**
  2862. * Helper function for FunctionDeclaration grammar rule.
  2863. */
  2864. private function CustomFunctionDeclaration(): ?Functions\FunctionNode
  2865. {
  2866. assert($this->lexer->lookahead !== null);
  2867. $token = $this->lexer->lookahead;
  2868. $funcName = strtolower($token->value);
  2869. // Check for custom functions afterwards
  2870. $config = $this->em->getConfiguration();
  2871. switch (true) {
  2872. case $config->getCustomStringFunction($funcName) !== null:
  2873. return $this->CustomFunctionsReturningStrings();
  2874. case $config->getCustomNumericFunction($funcName) !== null:
  2875. return $this->CustomFunctionsReturningNumerics();
  2876. case $config->getCustomDatetimeFunction($funcName) !== null:
  2877. return $this->CustomFunctionsReturningDatetime();
  2878. default:
  2879. return null;
  2880. }
  2881. }
  2882. /**
  2883. * FunctionsReturningNumerics ::=
  2884. * "LENGTH" "(" StringPrimary ")" |
  2885. * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
  2886. * "ABS" "(" SimpleArithmeticExpression ")" |
  2887. * "SQRT" "(" SimpleArithmeticExpression ")" |
  2888. * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
  2889. * "SIZE" "(" CollectionValuedPathExpression ")" |
  2890. * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
  2891. * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
  2892. * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
  2893. *
  2894. * @return Functions\FunctionNode
  2895. */
  2896. public function FunctionsReturningNumerics()
  2897. {
  2898. assert($this->lexer->lookahead !== null);
  2899. $funcNameLower = strtolower($this->lexer->lookahead->value);
  2900. $funcClass = self::$numericFunctions[$funcNameLower];
  2901. $function = new $funcClass($funcNameLower);
  2902. $function->parse($this);
  2903. return $function;
  2904. }
  2905. /** @return Functions\FunctionNode */
  2906. public function CustomFunctionsReturningNumerics()
  2907. {
  2908. assert($this->lexer->lookahead !== null);
  2909. // getCustomNumericFunction is case-insensitive
  2910. $functionName = strtolower($this->lexer->lookahead->value);
  2911. $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
  2912. assert($functionClass !== null);
  2913. $function = is_string($functionClass)
  2914. ? new $functionClass($functionName)
  2915. : $functionClass($functionName);
  2916. $function->parse($this);
  2917. return $function;
  2918. }
  2919. /**
  2920. * FunctionsReturningDatetime ::=
  2921. * "CURRENT_DATE" |
  2922. * "CURRENT_TIME" |
  2923. * "CURRENT_TIMESTAMP" |
  2924. * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
  2925. * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
  2926. *
  2927. * @return Functions\FunctionNode
  2928. */
  2929. public function FunctionsReturningDatetime()
  2930. {
  2931. assert($this->lexer->lookahead !== null);
  2932. $funcNameLower = strtolower($this->lexer->lookahead->value);
  2933. $funcClass = self::$datetimeFunctions[$funcNameLower];
  2934. $function = new $funcClass($funcNameLower);
  2935. $function->parse($this);
  2936. return $function;
  2937. }
  2938. /** @return Functions\FunctionNode */
  2939. public function CustomFunctionsReturningDatetime()
  2940. {
  2941. assert($this->lexer->lookahead !== null);
  2942. // getCustomDatetimeFunction is case-insensitive
  2943. $functionName = $this->lexer->lookahead->value;
  2944. $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
  2945. assert($functionClass !== null);
  2946. $function = is_string($functionClass)
  2947. ? new $functionClass($functionName)
  2948. : $functionClass($functionName);
  2949. $function->parse($this);
  2950. return $function;
  2951. }
  2952. /**
  2953. * FunctionsReturningStrings ::=
  2954. * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
  2955. * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
  2956. * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
  2957. * "LOWER" "(" StringPrimary ")" |
  2958. * "UPPER" "(" StringPrimary ")" |
  2959. * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
  2960. *
  2961. * @return Functions\FunctionNode
  2962. */
  2963. public function FunctionsReturningStrings()
  2964. {
  2965. assert($this->lexer->lookahead !== null);
  2966. $funcNameLower = strtolower($this->lexer->lookahead->value);
  2967. $funcClass = self::$stringFunctions[$funcNameLower];
  2968. $function = new $funcClass($funcNameLower);
  2969. $function->parse($this);
  2970. return $function;
  2971. }
  2972. /** @return Functions\FunctionNode */
  2973. public function CustomFunctionsReturningStrings()
  2974. {
  2975. assert($this->lexer->lookahead !== null);
  2976. // getCustomStringFunction is case-insensitive
  2977. $functionName = $this->lexer->lookahead->value;
  2978. $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
  2979. assert($functionClass !== null);
  2980. $function = is_string($functionClass)
  2981. ? new $functionClass($functionName)
  2982. : $functionClass($functionName);
  2983. $function->parse($this);
  2984. return $function;
  2985. }
  2986. private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata
  2987. {
  2988. if (! isset($this->queryComponents[$dqlAlias]['metadata'])) {
  2989. throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias));
  2990. }
  2991. return $this->queryComponents[$dqlAlias]['metadata'];
  2992. }
  2993. }