Commit | Line | Data |
---|---|---|
22601c92 CW |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
4 | | Copyright CiviCRM LLC. All rights reserved. | | |
5 | | | | |
6 | | This work is published under the GNU AGPLv3 license with some | | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
9 | +--------------------------------------------------------------------+ | |
10 | */ | |
11 | ||
12 | namespace Civi\Search; | |
13 | ||
14 | /** | |
15 | * Class Admin | |
16 | * @package Civi\Search | |
17 | */ | |
18 | class Admin { | |
19 | ||
20 | /** | |
21 | * @return array | |
22 | */ | |
23 | public static function getAdminSettings():array { | |
4f0729ed | 24 | $schema = self::getSchema(); |
22601c92 | 25 | return [ |
4f0729ed CW |
26 | 'schema' => $schema, |
27 | 'joins' => self::getJoins(array_column($schema, NULL, 'name')), | |
22601c92 CW |
28 | 'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()), |
29 | 'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(), | |
ecb9d1eb | 30 | 'displayTypes' => Display::getDisplayTypes(['id', 'name', 'label', 'description', 'icon']), |
2ef64700 CW |
31 | 'afformEnabled' => (bool) \CRM_Utils_Array::findAll( |
32 | \CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(), | |
33 | ['fullName' => 'org.civicrm.afform'] | |
34 | ), | |
35 | 'afformAdminEnabled' => (bool) \CRM_Utils_Array::findAll( | |
36 | \CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(), | |
37 | ['fullName' => 'org.civicrm.afform_admin'] | |
38 | ), | |
22601c92 CW |
39 | ]; |
40 | } | |
41 | ||
42 | /** | |
43 | * @return string[] | |
44 | */ | |
45 | public static function getOperators():array { | |
46 | return [ | |
47 | '=' => '=', | |
48 | '!=' => '≠', | |
49 | '>' => '>', | |
50 | '<' => '<', | |
51 | '>=' => '≥', | |
52 | '<=' => '≤', | |
53 | 'CONTAINS' => ts('Contains'), | |
014174e7 CW |
54 | 'IN' => ts('Is One Of'), |
55 | 'NOT IN' => ts('Not One Of'), | |
22601c92 CW |
56 | 'LIKE' => ts('Is Like'), |
57 | 'NOT LIKE' => ts('Not Like'), | |
58 | 'BETWEEN' => ts('Is Between'), | |
59 | 'NOT BETWEEN' => ts('Not Between'), | |
60 | 'IS NULL' => ts('Is Null'), | |
61 | 'IS NOT NULL' => ts('Not Null'), | |
62 | ]; | |
63 | } | |
64 | ||
65 | /** | |
66 | * Fetch all entities the current user has permission to `get` | |
590c0e3f | 67 | * @return array |
22601c92 CW |
68 | */ |
69 | public static function getSchema() { | |
70 | $schema = []; | |
71 | $entities = \Civi\Api4\Entity::get() | |
861688db | 72 | ->addSelect('name', 'title', 'type', 'title_plural', 'description', 'label_field', 'icon', 'paths', 'dao', 'bridge', 'ui_join_filters') |
09815e9c | 73 | ->addWhere('searchable', '=', TRUE) |
44111498 | 74 | ->addOrderBy('title_plural') |
22601c92 CW |
75 | ->setChain([ |
76 | 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']], | |
77 | ])->execute(); | |
30d895a9 | 78 | $getFields = ['name', 'title', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity']; |
22601c92 CW |
79 | foreach ($entities as $entity) { |
80 | // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get | |
81 | if ($entity['get']) { | |
f9cf8797 CW |
82 | // Add paths (but only RUD actions) with translated titles |
83 | foreach ($entity['paths'] as $action => $path) { | |
84 | unset($entity['paths'][$action]); | |
85 | switch ($action) { | |
86 | case 'view': | |
87 | $title = ts('View %1', [1 => $entity['title']]); | |
88 | break; | |
89 | ||
5c385299 | 90 | case 'update': |
f9cf8797 CW |
91 | $title = ts('Edit %1', [1 => $entity['title']]); |
92 | break; | |
93 | ||
94 | case 'delete': | |
95 | $title = ts('Delete %1', [1 => $entity['title']]); | |
96 | break; | |
97 | ||
98 | default: | |
99 | continue 2; | |
100 | } | |
101 | $entity['paths'][] = [ | |
102 | 'path' => $path, | |
103 | 'title' => $title, | |
104 | 'action' => $action, | |
105 | ]; | |
106 | } | |
4f0729ed | 107 | $entity['fields'] = (array) civicrm_api4($entity['name'], 'getFields', [ |
22601c92 | 108 | 'select' => $getFields, |
393840b9 | 109 | 'where' => [['name', 'NOT IN', ['api_key', 'hash']]], |
22601c92 CW |
110 | 'orderBy' => ['label'], |
111 | ]); | |
112 | $params = $entity['get'][0]; | |
113 | // Entity must support at least these params or it is too weird for search kit | |
114 | if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) { | |
115 | \CRM_Utils_Array::remove($params, 'checkPermissions', 'debug', 'chain', 'language', 'select', 'where', 'orderBy', 'limit', 'offset'); | |
116 | unset($entity['get']); | |
861688db | 117 | $schema[$entity['name']] = ['params' => array_keys($params)] + array_filter($entity); |
22601c92 CW |
118 | } |
119 | } | |
120 | } | |
861688db CW |
121 | // Add in FK fields for implicit joins |
122 | // For example, add a `campaign.title` field to the Contribution entity | |
123 | foreach ($schema as &$entity) { | |
124 | foreach (array_reverse($entity['fields'], TRUE) as $index => $field) { | |
125 | if (!empty($field['fk_entity']) && !$field['options'] && !empty($schema[$field['fk_entity']]['label_field'])) { | |
126 | // The original field will get title instead of label since it represents the id (title usually ends in ID but label does not) | |
127 | $entity['fields'][$index]['label'] = $field['title']; | |
128 | // Add the label field from the other entity to this entity's list of fields | |
129 | $newField = \CRM_Utils_Array::findAll($schema[$field['fk_entity']]['fields'], ['name' => $schema[$field['fk_entity']]['label_field']])[0]; | |
130 | $newField['name'] = str_replace('_id', '', $field['name']) . '.' . $schema[$field['fk_entity']]['label_field']; | |
131 | $newField['label'] = $field['label'] . ' ' . $newField['label']; | |
132 | array_splice($entity['fields'], $index, 0, [$newField]); | |
133 | } | |
134 | } | |
135 | } | |
136 | return array_values($schema); | |
22601c92 CW |
137 | } |
138 | ||
139 | /** | |
590c0e3f | 140 | * @param array $allowedEntities |
22601c92 CW |
141 | * @return array |
142 | */ | |
4f0729ed CW |
143 | public static function getJoins(array $allowedEntities) { |
144 | $joins = []; | |
145 | foreach ($allowedEntities as $entity) { | |
5828ae54 CW |
146 | // Multi-record custom field groups (to-date only the contact entity supports these) |
147 | if (in_array('CustomValue', $entity['type'])) { | |
148 | $targetEntity = $allowedEntities['Contact']; | |
149 | // Join from Custom group to Contact (n-1) | |
150 | $alias = $entity['name'] . '_Contact_entity_id'; | |
151 | $joins[$entity['name']][] = [ | |
152 | 'label' => $entity['title'] . ' ' . $targetEntity['title'], | |
153 | 'description' => '', | |
154 | 'entity' => 'Contact', | |
155 | 'conditions' => self::getJoinConditions('entity_id', $alias . '.id'), | |
156 | 'defaults' => self::getJoinDefaults($alias, $targetEntity), | |
157 | 'alias' => $alias, | |
158 | 'multi' => FALSE, | |
159 | ]; | |
160 | // Join from Contact to Custom group (n-n) | |
161 | $alias = 'Contact_' . $entity['name'] . '_entity_id'; | |
162 | $joins['Contact'][] = [ | |
163 | 'label' => $entity['title_plural'], | |
164 | 'description' => '', | |
165 | 'entity' => $entity['name'], | |
166 | 'conditions' => self::getJoinConditions('id', $alias . '.entity_id'), | |
167 | 'defaults' => self::getJoinDefaults($alias, $entity), | |
168 | 'alias' => $alias, | |
169 | 'multi' => TRUE, | |
170 | ]; | |
171 | } | |
172 | // Non-custom DAO entities | |
173 | elseif (!empty($entity['dao'])) { | |
4f0729ed CW |
174 | /* @var \CRM_Core_DAO $daoClass */ |
175 | $daoClass = $entity['dao']; | |
176 | $references = $daoClass::getReferenceColumns(); | |
177 | // Only the first bridge reference gets processed, so if it's dynamic we want to be sure it's first in the list | |
178 | usort($references, function($reference) { | |
179 | return is_a($reference, 'CRM_Core_Reference_Dynamic') ? -1 : 1; | |
180 | }); | |
181 | $fields = array_column($entity['fields'], NULL, 'name'); | |
182 | $bridge = in_array('EntityBridge', $entity['type']) ? $entity['name'] : NULL; | |
4f0729ed CW |
183 | foreach ($references as $reference) { |
184 | $keyField = $fields[$reference->getReferenceKey()] ?? NULL; | |
185 | // Exclude any joins that are better represented by pseudoconstants | |
186 | if (is_a($reference, 'CRM_Core_Reference_OptionValue') | |
187 | || !$keyField || !empty($keyField['options']) | |
188 | // Limit bridge joins to just the first | |
189 | || $bridge && array_search($keyField['name'], $entity['bridge']) !== 0 | |
190 | // Sanity check - table should match | |
191 | || $daoClass::getTableName() !== $reference->getReferenceTable() | |
192 | ) { | |
193 | continue; | |
194 | } | |
5828ae54 CW |
195 | // Dynamic references use a column like "entity_table" (for normal joins this value will be null) |
196 | $dynamicCol = $reference->getTypeColumn(); | |
2f616560 | 197 | // For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once |
17019d49 | 198 | foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) { |
4f0729ed CW |
199 | if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) { |
200 | continue; | |
201 | } | |
202 | $targetEntity = $allowedEntities[$targetEntityName]; | |
552bb940 | 203 | // Non-bridge joins directly between 2 entities |
4f0729ed CW |
204 | if (!$bridge) { |
205 | // Add the straight 1-1 join | |
206 | $alias = $entity['name'] . '_' . $targetEntityName . '_' . $keyField['name']; | |
207 | $joins[$entity['name']][] = [ | |
208 | 'label' => $entity['title'] . ' ' . $targetEntity['title'], | |
209 | 'description' => $dynamicCol ? '' : $keyField['label'], | |
210 | 'entity' => $targetEntityName, | |
211 | 'conditions' => self::getJoinConditions($keyField['name'], $alias . '.' . $reference->getTargetKey(), $targetTable, $dynamicCol), | |
2f616560 | 212 | 'defaults' => self::getJoinDefaults($alias, $targetEntity), |
4f0729ed CW |
213 | 'alias' => $alias, |
214 | 'multi' => FALSE, | |
215 | ]; | |
216 | // Flip the conditions & add the reverse (1-n) join | |
217 | $alias = $targetEntityName . '_' . $entity['name'] . '_' . $keyField['name']; | |
218 | $joins[$targetEntityName][] = [ | |
219 | 'label' => $targetEntity['title'] . ' ' . $entity['title_plural'], | |
220 | 'description' => $dynamicCol ? '' : $keyField['label'], | |
221 | 'entity' => $entity['name'], | |
222 | 'conditions' => self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL), | |
2f616560 | 223 | 'defaults' => self::getJoinDefaults($alias, $entity), |
4f0729ed CW |
224 | 'alias' => $alias, |
225 | 'multi' => TRUE, | |
226 | ]; | |
227 | } | |
552bb940 CW |
228 | // Bridge joins (sanity check - bridge must specify exactly 2 FK fields) |
229 | elseif (count($entity['bridge']) === 2) { | |
230 | // Get the other entity being linked through this bridge | |
231 | $baseKey = array_search($reference->getReferenceKey(), $entity['bridge']) ? $entity['bridge'][0] : $entity['bridge'][1]; | |
232 | $baseEntity = $allowedEntities[$fields[$baseKey]['fk_entity']] ?? NULL; | |
233 | if (!$baseEntity) { | |
234 | continue; | |
235 | } | |
4f0729ed CW |
236 | // Add joins for the two entities that connect through this bridge (n-n) |
237 | $symmetric = $baseEntity['name'] === $targetEntityName; | |
238 | $targetsTitle = $symmetric ? $allowedEntities[$bridge]['title_plural'] : $targetEntity['title_plural']; | |
552bb940 | 239 | $alias = $baseEntity['name'] . "_{$bridge}_" . $targetEntityName; |
4f0729ed CW |
240 | $joins[$baseEntity['name']][] = [ |
241 | 'label' => $baseEntity['title'] . ' ' . $targetsTitle, | |
242 | 'description' => ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]), | |
243 | 'entity' => $targetEntityName, | |
552bb940 CW |
244 | 'conditions' => array_merge( |
245 | [$bridge], | |
246 | self::getJoinConditions('id', $alias . '.' . $baseKey, NULL, NULL) | |
247 | ), | |
2f616560 | 248 | 'defaults' => self::getJoinDefaults($alias, $targetEntity, $entity), |
4f0729ed | 249 | 'bridge' => $bridge, |
552bb940 | 250 | 'alias' => $alias, |
4f0729ed CW |
251 | 'multi' => TRUE, |
252 | ]; | |
253 | if (!$symmetric) { | |
552bb940 | 254 | $alias = $targetEntityName . "_{$bridge}_" . $baseEntity['name']; |
4f0729ed CW |
255 | $joins[$targetEntityName][] = [ |
256 | 'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'], | |
257 | 'description' => ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]), | |
258 | 'entity' => $baseEntity['name'], | |
552bb940 CW |
259 | 'conditions' => array_merge( |
260 | [$bridge], | |
261 | self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL) | |
262 | ), | |
2f616560 | 263 | 'defaults' => self::getJoinDefaults($alias, $baseEntity, $entity), |
4f0729ed | 264 | 'bridge' => $bridge, |
552bb940 | 265 | 'alias' => $alias, |
4f0729ed CW |
266 | 'multi' => TRUE, |
267 | ]; | |
268 | } | |
269 | } | |
270 | } | |
22601c92 CW |
271 | } |
272 | } | |
22601c92 | 273 | } |
4f0729ed CW |
274 | return $joins; |
275 | } | |
276 | ||
277 | /** | |
278 | * Boilerplate join clause | |
279 | * | |
280 | * @param string $nearCol | |
281 | * @param string $farCol | |
282 | * @param string $targetTable | |
283 | * @param string|null $dynamicCol | |
284 | * @return array[] | |
285 | */ | |
5828ae54 | 286 | private static function getJoinConditions($nearCol, $farCol, $targetTable = NULL, $dynamicCol = NULL) { |
4f0729ed CW |
287 | $conditions = [ |
288 | [ | |
289 | $nearCol, | |
290 | '=', | |
291 | $farCol, | |
292 | ], | |
293 | ]; | |
294 | if ($dynamicCol) { | |
295 | $conditions[] = [ | |
296 | $dynamicCol, | |
297 | '=', | |
298 | "'$targetTable'", | |
299 | ]; | |
300 | } | |
301 | return $conditions; | |
22601c92 CW |
302 | } |
303 | ||
2f616560 CW |
304 | /** |
305 | * @param $alias | |
306 | * @param array ...$entities | |
307 | * @return array | |
308 | */ | |
309 | private static function getJoinDefaults($alias, ...$entities):array { | |
310 | $conditions = []; | |
311 | foreach ($entities as $entity) { | |
312 | foreach ($entity['ui_join_filters'] ?? [] as $fieldName) { | |
313 | $field = civicrm_api4($entity['name'], 'getFields', [ | |
314 | 'select' => ['options'], | |
315 | 'where' => [['name', '=', $fieldName]], | |
316 | 'loadOptions' => ['name'], | |
317 | ])->first(); | |
318 | $value = isset($field['options'][0]) ? json_encode($field['options'][0]['name']) : ''; | |
319 | $conditions[] = [ | |
320 | $alias . '.' . $fieldName . ($value ? ':name' : ''), | |
321 | '=', | |
322 | $value, | |
323 | ]; | |
324 | } | |
325 | } | |
326 | return $conditions; | |
327 | } | |
328 | ||
22601c92 | 329 | } |