SearchKit - Fix aggregation issues
[civicrm-core.git] / ext / search_kit / Civi / Api4 / Action / SearchDisplay / SavedSearchInspectorTrait.php
CommitLineData
ea04af0c
CW
1<?php
2
3namespace Civi\Api4\Action\SearchDisplay;
4
49208bdb 5use Civi\API\Request;
ea04af0c
CW
6use Civi\Api4\Query\SqlExpression;
7use Civi\Api4\SavedSearch;
8use Civi\Api4\Utils\CoreUtil;
9
10/**
11 * Trait for requiring a savedSearch as a param plus util functions for inspecting it.
12 *
13 * @method $this setSavedSearch(array|string $savedSearch)
14 * @method array|string getSavedSearch()
15 * @package Civi\Api4\Action\SearchDisplay
16 */
17trait SavedSearchInspectorTrait {
18
19 /**
20 * Either the name of the savedSearch or an array containing the savedSearch definition (for preview mode)
21 * @var string|array
22 * @required
23 */
24 protected $savedSearch;
25
7193d9f6
CW
26 /**
27 * @var array{select: array, where: array, having: array, orderBy: array, limit: int, offset: int, checkPermissions: bool, debug: bool}
28 */
29 protected $_apiParams;
30
ea04af0c
CW
31 /**
32 * @var \Civi\Api4\Query\Api4SelectQuery
33 */
34 private $_selectQuery;
35
36 /**
37 * @var array
38 */
39 private $_selectClause;
40
49208bdb
CW
41 /**
42 * @var array
43 */
44 private $_searchEntityFields;
45
ea04af0c
CW
46 /**
47 * If SavedSearch is supplied as a string, this will load it as an array
48 * @throws \API_Exception
49 * @throws \Civi\API\Exception\UnauthorizedException
50 */
51 protected function loadSavedSearch() {
52 if (is_string($this->savedSearch)) {
53 $this->savedSearch = SavedSearch::get(FALSE)
54 ->addWhere('name', '=', $this->savedSearch)
55 ->execute()->single();
56 }
49208bdb 57 $this->_apiParams = ($this->savedSearch['api_params'] ?? []) + ['select' => [], 'where' => []];
ea04af0c
CW
58 }
59
60 /**
61 * Returns field definition for a given field or NULL if not found
62 * @param $fieldName
63 * @return array|null
64 */
65 protected function getField($fieldName) {
49208bdb
CW
66 [$fieldName] = explode(':', $fieldName);
67 return $this->getQuery() ?
68 $this->getQuery()->getField($fieldName, FALSE) :
69 ($this->getEntityFields()[$fieldName] ?? NULL);
ea04af0c
CW
70 }
71
72 /**
73 * @param $joinAlias
74 * @return array{entity: string, alias: string, table: string, bridge: string|NULL}|NULL
75 */
76 protected function getJoin($joinAlias) {
49208bdb 77 return $this->getQuery() ? $this->getQuery()->getExplicitJoin($joinAlias) : NULL;
ea04af0c
CW
78 }
79
80 /**
81 * @return array{entity: string, alias: string, table: string, bridge: string|NULL}[]
82 */
83 protected function getJoins() {
84 return $this->getQuery() ? $this->getQuery()->getExplicitJoins() : [];
85 }
86
87 /**
49208bdb
CW
88 * Returns a Query object for the search entity, or FALSE if it doesn't have a DAO
89 *
90 * @return \Civi\Api4\Query\Api4SelectQuery|bool
ea04af0c
CW
91 */
92 private function getQuery() {
49208bdb
CW
93 if (!isset($this->_selectQuery) && !empty($this->savedSearch['api_entity'])) {
94 if (!in_array('DAOEntity', CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'type'), TRUE)) {
95 return $this->_selectQuery = FALSE;
96 }
97 $api = Request::create($this->savedSearch['api_entity'], 'get', $this->savedSearch['api_params']);
ea04af0c
CW
98 $this->_selectQuery = new \Civi\Api4\Query\Api4SelectQuery($api);
99 }
100 return $this->_selectQuery;
101 }
102
49208bdb
CW
103 /**
104 * Used as a fallback for non-DAO entities which don't use the Query object
105 *
106 * @return array
107 */
108 private function getEntityFields() {
109 if (!isset($this->_searchEntityFields)) {
110 $this->_searchEntityFields = Request::create($this->savedSearch['api_entity'], 'get', $this->savedSearch['api_params'])
111 ->entityFields();
112 }
113 return $this->_searchEntityFields;
114 }
115
ea04af0c
CW
116 /**
117 * Returns the select clause enhanced with metadata
118 *
119 * @return array{fields: array, expr: SqlExpression, dataType: string}[]
120 */
121 protected function getSelectClause() {
122 if (!isset($this->_selectClause)) {
123 $this->_selectClause = [];
7193d9f6 124 foreach ($this->_apiParams['select'] as $selectExpr) {
ea04af0c
CW
125 $expr = SqlExpression::convert($selectExpr, TRUE);
126 $item = [
127 'fields' => [],
128 'expr' => $expr,
129 'dataType' => $expr->getDataType(),
130 ];
131 foreach ($expr->getFields() as $fieldName) {
132 $fieldMeta = $this->getField($fieldName);
133 if ($fieldMeta) {
134 $item['fields'][] = $fieldMeta;
135 }
136 }
137 if (!isset($item['dataType']) && $item['fields']) {
138 $item['dataType'] = $item['fields'][0]['data_type'];
139 }
140 $this->_selectClause[$expr->getAlias()] = $item;
141 }
142 }
143 return $this->_selectClause;
144 }
145
146 /**
147 * @param string $key
148 * @return array{fields: array, expr: SqlExpression, dataType: string}|NULL
149 */
150 protected function getSelectExpression($key) {
151 return $this->getSelectClause()[$key] ?? NULL;
152 }
153
154 /**
155 * Determines if a column belongs to an aggregate grouping
156 * @param string $fieldPath
157 * @return bool
158 */
159 private function canAggregate($fieldPath) {
587f3877
CW
160 // Disregard suffix
161 [$fieldPath] = explode(':', $fieldPath);
ea04af0c 162 $field = $this->getField($fieldPath);
587f3877 163 $apiParams = $this->savedSearch['api_params'] ?? [];
ea04af0c
CW
164
165 // If the query does not use grouping or the field doesn't exist, never
166 if (empty($apiParams['groupBy']) || !$field) {
167 return FALSE;
168 }
169 // If the column is used for a groupBy, no
170 if (in_array($fieldPath, $apiParams['groupBy'])) {
171 return FALSE;
172 }
173
174 // If the entity this column belongs to is being grouped by id, then also no
587f3877 175 $idField = substr($fieldPath, 0, 0 - strlen($field['name'])) . CoreUtil::getIdFieldName($field['entity']);
ea04af0c
CW
176 return !in_array($idField, $apiParams['groupBy']);
177 }
178
179}