From ee11a31f16fc4c2746ac044d217c4af11dba1eec Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Dec 2024 10:52:26 -0800 Subject: [PATCH 1/2] Parse the where param for column mappings --- src/elements/db/ElementQuery.php | 183 +++++++++++++++++++++++-------- 1 file changed, 138 insertions(+), 45 deletions(-) diff --git a/src/elements/db/ElementQuery.php b/src/elements/db/ElementQuery.php index cb5f143060b..6eed4bc34c0 100644 --- a/src/elements/db/ElementQuery.php +++ b/src/elements/db/ElementQuery.php @@ -1615,6 +1615,10 @@ public function prepare($builder): Query 'uid' => 'elements.uid', ]; + if ($class::hasTitles()) { + $this->_columnMap['title'] = 'elements_sites.title'; + } + // Keep track of whether an element table is joined into the query $this->_joinedElementTable = false; @@ -1623,6 +1627,10 @@ public function prepare($builder): Query throw new QueryAbortedException(); } + // Map custom field handles to their content values + $this->customFields = $this->customFields(); + $this->_addCustomFieldsToColumnMap($builder->db); + $this->subQuery ->addSelect([ 'elementsId' => 'elements.id', @@ -1630,17 +1638,17 @@ public function prepare($builder): Query ]) ->from(['elements' => Table::ELEMENTS]) ->innerJoin(['elements_sites' => Table::ELEMENTS_SITES], '[[elements_sites.elementId]] = [[elements.id]]') - ->andWhere($this->where) ->offset($this->offset) ->limit($this->limit) ->addParams($this->params); + $this->_applyWhereParam(); + if (Craft::$app->getIsMultiSite(false, true)) { $this->subQuery->andWhere(['elements_sites.siteId' => $this->siteId]); } - $this->customFields = $this->customFields(); - $this->_loopInCustomFields(); + $this->_applyCustomFieldParams(); if ($this->distinct) { $this->query->distinct(); @@ -1703,47 +1711,6 @@ public function prepare($builder): Query $this->subQuery->andWhere(Db::parseParam('elements_sites.uri', $this->uri, '=', true)); } - if ($class::hasTitles()) { - $this->_columnMap['title'] = 'elements_sites.title'; - } - - // Map custom field handles to their content values - $isMysql = $builder->db->getIsMysql(); - foreach ($this->customFields as $field) { - $dbTypes = $field::dbType(); - - if ($dbTypes !== null) { - if (is_string($dbTypes)) { - $dbTypes = ['*' => $dbTypes]; - } else { - $dbTypes = [ - '*' => reset($dbTypes), - ...$dbTypes, - ]; - } - - foreach ($dbTypes as $key => $dbType) { - $alias = $field->handle . ($key !== '*' ? ".$key" : ''); - $resolver = fn() => $field->getValueSql($key !== '*' ? $key : null); - - if (isset($this->_columnMap[$alias])) { - if (!is_array($this->_columnMap[$alias])) { - $this->_columnMap[$alias] = [$this->_columnMap[$alias]]; - } - $this->_columnMap[$alias][] = $resolver; - } else { - $this->_columnMap[$alias] = $resolver; - } - - // for mysql, we have to make sure text column type is cast to char, otherwise it won't be sorted correctly - // see https://github.com/craftcms/cms/issues/15609 - if ($isMysql && Db::parseColumnType($dbType) === Schema::TYPE_TEXT) { - $this->_columnsToCast[$alias] = 'CHAR(255)'; - } - } - } - } - $this->_applyRelatedToParam(); $this->_applyNotRelatedToParam(); $this->_applyStructureParams($class); @@ -2680,12 +2647,55 @@ private function _placeholderCondition(mixed $condition): mixed return ['or', $condition, $this->_placeholderCondition]; } + /** + * Include custom fields in the column map + */ + private function _addCustomFieldsToColumnMap(Connection $db): void + { + $isMysql = $db->getIsMysql(); + + foreach ($this->customFields as $field) { + $dbTypes = $field::dbType(); + + if ($dbTypes !== null) { + if (is_string($dbTypes)) { + $dbTypes = ['*' => $dbTypes]; + } else { + $dbTypes = [ + '*' => reset($dbTypes), + ...$dbTypes, + ]; + } + + foreach ($dbTypes as $key => $dbType) { + $alias = $field->handle . ($key !== '*' ? ".$key" : ''); + $resolver = fn() => $field->getValueSql($key !== '*' ? $key : null); + + if (isset($this->_columnMap[$alias])) { + if (!is_array($this->_columnMap[$alias])) { + $this->_columnMap[$alias] = [$this->_columnMap[$alias]]; + } + $this->_columnMap[$alias][] = $resolver; + } else { + $this->_columnMap[$alias] = $resolver; + } + + // for mysql, we have to make sure text column type is cast to char, otherwise it won't be sorted correctly + // see https://github.com/craftcms/cms/issues/15609 + if ($isMysql && Db::parseColumnType($dbType) === Schema::TYPE_TEXT) { + $this->_columnsToCast[$alias] = 'CHAR(255)'; + } + } + } + } + } + /** * Allow the custom fields to modify the query. * * @throws QueryAbortedException */ - private function _loopInCustomFields(): void + private function _applyCustomFieldParams(): void { if (is_array($this->customFields)) { $fieldAttributes = $this->getBehavior('customFields'); @@ -3553,6 +3563,89 @@ private function _applyUniqueParam(YiiConnection $db): void $this->subQuery->andWhere(new Expression("[[elements_sites.id]] = ($subSelectSql)")); } + /** + * Applies the `where` param to the query being prepraed. + */ + private function _applyWhereParam(): void + { + if (empty($this->where)) { + return; + } + + if (is_string($this->where)) { + $where = $this->_parseStringCondition($this->where); + } elseif (is_array($this->where)) { + $where = $this->_parseArrayCondition($this->where); + } else { + $where = $this->where; + } + + $this->subQuery->andWhere($where); + } + + private function _parseStringCondition(string $condition): string + { + if (!str_contains($condition, '[')) { + return $this->_resolveColumnMappingForCondition($condition) ?? $condition; + } + + return preg_replace_callback('/\[\[(\w+(?:\.\w+)?)]]/', function(array $match) { + $mapping = $this->_resolveColumnMappingForCondition($match[1]); + if ($mapping === null) { + return $match[0]; + } + if (preg_match('/^\w+(?:\.\w+)?$/', $mapping)) { + return "[[$mapping]]"; + } + return $mapping; + }, $condition); + } + + private function _resolveColumnMappingForCondition(string $str): ?string + { + if (!isset($this->_columnMap[$str])) { + return null; + } + + $column = $this->_resolveColumnMapping($str); + return is_array($column) + ? (new CoalesceColumnsExpression($column))->getSql($this->subQuery->params) + : $column; + } + + private function _parseArrayCondition(array $condition): array + { + $parsed = []; + + if (isset($condition[0])) { + // Operator format: [operator, ...operands] + $operator = $parsed[] = strtoupper(array_shift($condition)); + if (in_array($operator, ['NOT', 'AND', 'OR'])) { + foreach ($condition as $value) { + if (is_string($value)) { + $value = $this->_parseStringCondition($value); + } elseif (is_array($value)) { + $value = $this->_parseArrayCondition($value); + } + $parsed[] = $value; + } + } else { + if (isset($condition[0]) && is_string($condition[0])) { + $condition[0] = $this->_resolveColumnMappingForCondition($condition[0]) ?? $condition[0]; + } + array_push($parsed, ...$condition); + } + } else { + // Hash format: [column => value] + foreach ($condition as $key => $value) { + $key = $this->_resolveColumnMappingForCondition($key) ?? $key; + $parsed[$key] = $value; + } + } + + return $parsed; + } + /** * Converts found rows into element instances * From be050b40002df4ff8a098daef2f3dde381d5dfef Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Dec 2024 12:07:14 -0800 Subject: [PATCH 2/2] Release note [ci skip] --- CHANGELOG-WIP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index b0e8cfa3120..b9b4de76d02 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -46,6 +46,7 @@ - It’s now possible to pass nested custom field value keys into element queries’ `orderBy` and `select` params (e.g. `myDateField.tz`). ([#16157](https://github.com/craftcms/cms/discussions/16157)) - It’s now possible to set Link field values to arrays with `value` keys set to element instances or IDs. ([#16255](https://github.com/craftcms/cms/pull/16255)) - The `indexOf` Twig filter now has a `default` argument, which can be any integer or `null`. (`-1` by default for backwards compatibility.) +- It’s now possible to reference custom field handles in element queries’ `where` params. ([#16318](https://github.com/craftcms/cms/pull/16318)) ### Extensibility - Added `craft\base\Element::EVENT_DEFINE_ALT_ACTIONS`. ([#16294](https://github.com/craftcms/cms/pull/16294))