query = $query; $this->position = 0; return $this->buildNodeGroup(false); } private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup { $nodes = []; $nodeResult = $this->buildNextNode($isSubquery); while (null !== $nodeResult->node) { $nodes[] = $nodeResult->node; if ($nodeResult->isSubqueryEnd) { break; } $nodeResult = $this->buildNextNode($isSubquery); } return new NodeGroup($nodes, $prohibited); } private function buildNextNode(bool $isSubquery): NodeResult { $tokenUnderConstruction = ''; $inQuotes = false; $fieldName = ''; $prohibited = false; while ($this->position < strlen($this->query)) { $char = $this->query[$this->position]; // Log::debug(sprintf('Char #%d: %s', $this->position, $char)); // If we're in a quoted string, we treat all characters except another quote as ordinary characters if ($inQuotes) { if ('"' !== $char) { $tokenUnderConstruction .= $char; ++$this->position; continue; } // char is " ++$this->position; return new NodeResult( $this->createNode($tokenUnderConstruction, $fieldName, $prohibited), false ); } switch ($char) { case '-': if ('' === $tokenUnderConstruction) { // A minus sign at the beginning of a token indicates prohibition // Log::debug('Indicate prohibition'); $prohibited = true; } if ('' !== $tokenUnderConstruction) { // In any other location, it's just a normal character $tokenUnderConstruction .= $char; } break; case '"': if ('' === $tokenUnderConstruction) { // A quote sign at the beginning of a token indicates the start of a quoted string $inQuotes = true; } if ('' !== $tokenUnderConstruction) { // In any other location, it's just a normal character $tokenUnderConstruction .= $char; } break; case '(': if ('' === $tokenUnderConstruction) { // A left parentheses at the beginning of a token indicates the start of a subquery ++$this->position; return new NodeResult( $this->buildNodeGroup(true, $prohibited), false ); } // In any other location, it's just a normal character $tokenUnderConstruction .= $char; break; case ')': // A right parentheses while in a subquery means the subquery ended, // thus also signaling the end of any node currently being built if ($isSubquery) { ++$this->position; return new NodeResult( '' !== $tokenUnderConstruction ? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited) : null, true ); } // In any other location, it's just a normal character $tokenUnderConstruction .= $char; break; case ':': $skipNext = false; if ('' === $tokenUnderConstruction) { // @phpstan-ignore-line // In any other location, it's just a normal character $tokenUnderConstruction .= $char; $skipNext = true; } if ('' !== $tokenUnderConstruction && !$skipNext) { Log::debug(sprintf('Turns out that "%s" is a field name. Reset the token.', $tokenUnderConstruction)); // If we meet a colon with a left-hand side string, we know we're in a field and are about to set up the value $fieldName = $tokenUnderConstruction; $tokenUnderConstruction = ''; } break; case ' ': // A space indicates the end of a token construction if non-empty, otherwise it's just ignored if ('' !== $tokenUnderConstruction) { ++$this->position; return new NodeResult( $this->createNode($tokenUnderConstruction, $fieldName, $prohibited), false ); } break; default: $tokenUnderConstruction .= $char; } ++$this->position; } $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName ? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited) : null; return new NodeResult($finalNode, true); } private function createNode(string $token, string $fieldName, bool $prohibited): Node { if ('' !== $fieldName) { Log::debug(sprintf('Create FieldNode %s:%s (%s)', $fieldName, $token, var_export($prohibited, true))); $token = ltrim($token, ':"'); $token = rtrim($token, '"'); return new FieldNode(trim($fieldName), trim($token), $prohibited); } Log::debug(sprintf('Create StringNode "%s" (%s)', $token, var_export($prohibited, true))); return new StringNode(trim($token), $prohibited); } }