/** * @param Token $token * * @return string */ protected function renderToken(Token $token) { $type = $token->getType(); // handle edge cases switch ($type) { case "open": return "("; case "close": return ")"; case "group": return $this->renderGroupBy(); case "sort": return $this->renderSort(); case "function": /** @var Value $token */ return $this->renderFunction($token); } if ($token instanceof Entity) { return $this->renderEntity($token); } if ($token instanceof Reference) { return $this->renderReference($token); } if ($token instanceof Value) { return $this->renderValue($token); } return strtoupper($type); }
/** * queries with 2 or more includes require a series of select queries, unioned together, then sorted to produce an * array format that can be used to generate entities from * * @param Token $token * * @return string */ protected function renderIncludeQuery(Token $token) { $collections = $this->query->getIncludes(); $mainMetadata = $this->query->getEntityMetadata(); $mainCollection = $mainMetadata->getCollection(); $subqueryTemplateSql = "SELECT {{fieldList}} FROM " . $this->renderArbitraryReference($mainCollection); // parse the tokens for join conditions $joinConditions = []; $joinMap = []; while (!empty($token) && !in_array($token->getType(), ["group", "sort", "limit"])) { $subqueryTemplateSql .= " " . $this->renderToken($token); if ($token->getType() == "join") { /** @var Reference $ref */ $ref = $this->query->getNextToken(); $alias = $this->getReferenceAlias($ref); $subqueryTemplateSql .= " " . $this->renderToken($ref); $subqueryTemplateSql .= " " . $this->renderToken($this->query->getNextToken()); // ON $subqueryTemplateSql .= " " . $this->renderToken($this->query->getNextToken()); // ( // write the join condition to a separate array, so we can replace it into the SQL when appropriate $level = 1; // counter to keep track of how deep we go into any closures $joinCondition = ""; $joinCollectionsFound = []; while (($token = $this->query->getNextToken()) && !($token->getType() == "close" && $level == 1)) { /** @var Token $token */ $joinCondition .= " " . $this->renderToken($token); $type = $token->getType(); switch ($type) { case "field": /** @var Reference $token */ // save the collection from this reference $collection = $this->getCollectionFromReference($token); if (!empty($collection)) { $joinCollectionsFound[] = $collection; } break; case "open": ++$level; break; case "close": --$level; break; } } // if we found enough collections, create a mapping between them if (count($joinCollectionsFound) > 1) { $this->mapJoins($joinCollectionsFound, $joinMap, $collections); } // save the join condition and write a placeholder to the template SQL $joinConditions[$alias] = $joinCondition; $subqueryTemplateSql .= "%{$alias}Condition%"; $subqueryTemplateSql .= $this->renderToken($token); // ) } $token = $this->query->getNextToken(); } // add target entity to the collections array (as the first element to make sorting easier later) unset($collections[$mainCollection]); $collections = array_merge([$mainCollection => $mainMetadata], $collections); $primaryKeys = $this->getPrimaryKeys($collections); // recursively construct the join tree $joinTree = $this->constructJoinTree($joinMap, $mainCollection); // recursively parse the tree into join lists $joinLists = $this->parseJoinTree($joinTree, $collections); // create the SQL for each subquery $subqueries = []; foreach ($joinLists as $collectionList) { $templateJoinConditions = $joinConditions; $replacements = []; foreach ($templateJoinConditions as $collection => $condition) { // if this is a collection that were including and that isn't in the collection list, set it's join condition to "[primary key] = NULL" if (isset($collections[$collection]) && !in_array($collection, $collectionList)) { $condition = "FALSE"; } $replacements["%{$collection}Condition%"] = $condition; } $subqueries[] = strtr($subqueryTemplateSql, $replacements); } $sortData = $this->processSortFields($token, $collections, $mainCollection, $primaryKeys); $sortSql = empty($sortData["sort"]) ? "" : " ORDER BY " . implode(", ", $sortData["sort"]); $limitSort = empty($sortData["limitSort"]) ? "" : " ORDER BY " . implode(", ", $sortData["limitSort"]); // process limits $mainAliasedPk = $this->getSelectFieldAlias($mainCollection . "." . $primaryKeys[$mainCollection]); $limitSubquery = $this->processIncludeQueryLimit($sortData["nextToken"], $joinConditions, $collections, $subqueryTemplateSql, $limitSort, $mainAliasedPk); $fieldList = implode(", ", array_merge($this->fields, $sortData["additionalFields"])); $sql = "SELECT s.* FROM (" . implode(" UNION ", $subqueries) . ") s"; $sql .= $limitSubquery; $sql = str_replace("{{fieldList}}", $fieldList, $sql); // add the sort SQL, but with the fields aliased to the "s" table $sql .= $sortSql; return $sql; }
/** * wraps the following expression in parentheses to isolate it * @param TokenSequencer|Token|mixed $content * @return $this */ public function closure($content = null) { $this->addNewToSequence("open"); if (!empty($content)) { if ($content instanceof TokenSequencer) { $this->mergeSequence($content->getSequence()); } elseif ($content instanceof Token) { $this->addToSequence($content); } elseif (is_array($content)) { foreach ($content as $subContent) { $this->addMixedContentToSequence($subContent); } } else { $this->val($content); } } $this->addNewToSequence("close"); return $this; }