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(), | |
e7515b5b | 30 | 'displayTypes' => Display::getDisplayTypes(['name', 'label', 'description', 'icon']), |
22601c92 CW |
31 | ]; |
32 | } | |
33 | ||
34 | /** | |
35 | * @return string[] | |
36 | */ | |
37 | public static function getOperators():array { | |
38 | return [ | |
39 | '=' => '=', | |
40 | '!=' => '≠', | |
41 | '>' => '>', | |
42 | '<' => '<', | |
43 | '>=' => '≥', | |
44 | '<=' => '≤', | |
45 | 'CONTAINS' => ts('Contains'), | |
014174e7 CW |
46 | 'IN' => ts('Is One Of'), |
47 | 'NOT IN' => ts('Not One Of'), | |
22601c92 CW |
48 | 'LIKE' => ts('Is Like'), |
49 | 'NOT LIKE' => ts('Not Like'), | |
50 | 'BETWEEN' => ts('Is Between'), | |
51 | 'NOT BETWEEN' => ts('Not Between'), | |
52 | 'IS NULL' => ts('Is Null'), | |
53 | 'IS NOT NULL' => ts('Not Null'), | |
54 | ]; | |
55 | } | |
56 | ||
57 | /** | |
58 | * Fetch all entities the current user has permission to `get` | |
590c0e3f | 59 | * @return array |
22601c92 CW |
60 | */ |
61 | public static function getSchema() { | |
62 | $schema = []; | |
63 | $entities = \Civi\Api4\Entity::get() | |
2f616560 | 64 | ->addSelect('name', 'title', 'type', 'title_plural', 'description', 'icon', 'paths', 'dao', 'bridge', 'ui_join_filters') |
09815e9c | 65 | ->addWhere('searchable', '=', TRUE) |
44111498 | 66 | ->addOrderBy('title_plural') |
22601c92 CW |
67 | ->setChain([ |
68 | 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']], | |
69 | ])->execute(); | |
70 | $getFields = ['name', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity']; | |
71 | foreach ($entities as $entity) { | |
72 | // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get | |
73 | if ($entity['get']) { | |
f9cf8797 CW |
74 | // Add paths (but only RUD actions) with translated titles |
75 | foreach ($entity['paths'] as $action => $path) { | |
76 | unset($entity['paths'][$action]); | |
77 | switch ($action) { | |
78 | case 'view': | |
79 | $title = ts('View %1', [1 => $entity['title']]); | |
80 | break; | |
81 | ||
5c385299 | 82 | case 'update': |
f9cf8797 CW |
83 | $title = ts('Edit %1', [1 => $entity['title']]); |
84 | break; | |
85 | ||
86 | case 'delete': | |
87 | $title = ts('Delete %1', [1 => $entity['title']]); | |
88 | break; | |
89 | ||
90 | default: | |
91 | continue 2; | |
92 | } | |
93 | $entity['paths'][] = [ | |
94 | 'path' => $path, | |
95 | 'title' => $title, | |
96 | 'action' => $action, | |
97 | ]; | |
98 | } | |
4f0729ed | 99 | $entity['fields'] = (array) civicrm_api4($entity['name'], 'getFields', [ |
22601c92 | 100 | 'select' => $getFields, |
393840b9 | 101 | 'where' => [['name', 'NOT IN', ['api_key', 'hash']]], |
22601c92 CW |
102 | 'orderBy' => ['label'], |
103 | ]); | |
104 | $params = $entity['get'][0]; | |
105 | // Entity must support at least these params or it is too weird for search kit | |
106 | if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) { | |
107 | \CRM_Utils_Array::remove($params, 'checkPermissions', 'debug', 'chain', 'language', 'select', 'where', 'orderBy', 'limit', 'offset'); | |
108 | unset($entity['get']); | |
109 | $schema[] = ['params' => array_keys($params)] + array_filter($entity); | |
110 | } | |
111 | } | |
112 | } | |
113 | return $schema; | |
114 | } | |
115 | ||
116 | /** | |
590c0e3f | 117 | * @param array $allowedEntities |
22601c92 CW |
118 | * @return array |
119 | */ | |
4f0729ed CW |
120 | public static function getJoins(array $allowedEntities) { |
121 | $joins = []; | |
122 | foreach ($allowedEntities as $entity) { | |
123 | if (!empty($entity['dao'])) { | |
124 | /* @var \CRM_Core_DAO $daoClass */ | |
125 | $daoClass = $entity['dao']; | |
126 | $references = $daoClass::getReferenceColumns(); | |
127 | // Only the first bridge reference gets processed, so if it's dynamic we want to be sure it's first in the list | |
128 | usort($references, function($reference) { | |
129 | return is_a($reference, 'CRM_Core_Reference_Dynamic') ? -1 : 1; | |
130 | }); | |
131 | $fields = array_column($entity['fields'], NULL, 'name'); | |
132 | $bridge = in_array('EntityBridge', $entity['type']) ? $entity['name'] : NULL; | |
4f0729ed CW |
133 | foreach ($references as $reference) { |
134 | $keyField = $fields[$reference->getReferenceKey()] ?? NULL; | |
135 | // Exclude any joins that are better represented by pseudoconstants | |
136 | if (is_a($reference, 'CRM_Core_Reference_OptionValue') | |
137 | || !$keyField || !empty($keyField['options']) | |
138 | // Limit bridge joins to just the first | |
139 | || $bridge && array_search($keyField['name'], $entity['bridge']) !== 0 | |
140 | // Sanity check - table should match | |
141 | || $daoClass::getTableName() !== $reference->getReferenceTable() | |
142 | ) { | |
143 | continue; | |
144 | } | |
2f616560 | 145 | // For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once |
17019d49 | 146 | foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) { |
4f0729ed CW |
147 | if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) { |
148 | continue; | |
149 | } | |
150 | $targetEntity = $allowedEntities[$targetEntityName]; | |
17019d49 CW |
151 | // Dynamic references use a column like "entity_table" |
152 | $dynamicCol = $reference->getTypeColumn(); | |
552bb940 | 153 | // Non-bridge joins directly between 2 entities |
4f0729ed CW |
154 | if (!$bridge) { |
155 | // Add the straight 1-1 join | |
156 | $alias = $entity['name'] . '_' . $targetEntityName . '_' . $keyField['name']; | |
157 | $joins[$entity['name']][] = [ | |
158 | 'label' => $entity['title'] . ' ' . $targetEntity['title'], | |
159 | 'description' => $dynamicCol ? '' : $keyField['label'], | |
160 | 'entity' => $targetEntityName, | |
161 | 'conditions' => self::getJoinConditions($keyField['name'], $alias . '.' . $reference->getTargetKey(), $targetTable, $dynamicCol), | |
2f616560 | 162 | 'defaults' => self::getJoinDefaults($alias, $targetEntity), |
4f0729ed CW |
163 | 'alias' => $alias, |
164 | 'multi' => FALSE, | |
165 | ]; | |
166 | // Flip the conditions & add the reverse (1-n) join | |
167 | $alias = $targetEntityName . '_' . $entity['name'] . '_' . $keyField['name']; | |
168 | $joins[$targetEntityName][] = [ | |
169 | 'label' => $targetEntity['title'] . ' ' . $entity['title_plural'], | |
170 | 'description' => $dynamicCol ? '' : $keyField['label'], | |
171 | 'entity' => $entity['name'], | |
172 | 'conditions' => self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL), | |
2f616560 | 173 | 'defaults' => self::getJoinDefaults($alias, $entity), |
4f0729ed CW |
174 | 'alias' => $alias, |
175 | 'multi' => TRUE, | |
176 | ]; | |
177 | } | |
552bb940 CW |
178 | // Bridge joins (sanity check - bridge must specify exactly 2 FK fields) |
179 | elseif (count($entity['bridge']) === 2) { | |
180 | // Get the other entity being linked through this bridge | |
181 | $baseKey = array_search($reference->getReferenceKey(), $entity['bridge']) ? $entity['bridge'][0] : $entity['bridge'][1]; | |
182 | $baseEntity = $allowedEntities[$fields[$baseKey]['fk_entity']] ?? NULL; | |
183 | if (!$baseEntity) { | |
184 | continue; | |
185 | } | |
4f0729ed CW |
186 | // Add joins for the two entities that connect through this bridge (n-n) |
187 | $symmetric = $baseEntity['name'] === $targetEntityName; | |
188 | $targetsTitle = $symmetric ? $allowedEntities[$bridge]['title_plural'] : $targetEntity['title_plural']; | |
552bb940 | 189 | $alias = $baseEntity['name'] . "_{$bridge}_" . $targetEntityName; |
4f0729ed CW |
190 | $joins[$baseEntity['name']][] = [ |
191 | 'label' => $baseEntity['title'] . ' ' . $targetsTitle, | |
192 | 'description' => ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]), | |
193 | 'entity' => $targetEntityName, | |
552bb940 CW |
194 | 'conditions' => array_merge( |
195 | [$bridge], | |
196 | self::getJoinConditions('id', $alias . '.' . $baseKey, NULL, NULL) | |
197 | ), | |
2f616560 | 198 | 'defaults' => self::getJoinDefaults($alias, $targetEntity, $entity), |
4f0729ed | 199 | 'bridge' => $bridge, |
552bb940 | 200 | 'alias' => $alias, |
4f0729ed CW |
201 | 'multi' => TRUE, |
202 | ]; | |
203 | if (!$symmetric) { | |
552bb940 | 204 | $alias = $targetEntityName . "_{$bridge}_" . $baseEntity['name']; |
4f0729ed CW |
205 | $joins[$targetEntityName][] = [ |
206 | 'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'], | |
207 | 'description' => ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]), | |
208 | 'entity' => $baseEntity['name'], | |
552bb940 CW |
209 | 'conditions' => array_merge( |
210 | [$bridge], | |
211 | self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL) | |
212 | ), | |
2f616560 | 213 | 'defaults' => self::getJoinDefaults($alias, $baseEntity, $entity), |
4f0729ed | 214 | 'bridge' => $bridge, |
552bb940 | 215 | 'alias' => $alias, |
4f0729ed CW |
216 | 'multi' => TRUE, |
217 | ]; | |
218 | } | |
219 | } | |
220 | } | |
22601c92 CW |
221 | } |
222 | } | |
22601c92 | 223 | } |
4f0729ed CW |
224 | return $joins; |
225 | } | |
226 | ||
227 | /** | |
228 | * Boilerplate join clause | |
229 | * | |
230 | * @param string $nearCol | |
231 | * @param string $farCol | |
232 | * @param string $targetTable | |
233 | * @param string|null $dynamicCol | |
234 | * @return array[] | |
235 | */ | |
236 | private static function getJoinConditions($nearCol, $farCol, $targetTable, $dynamicCol) { | |
237 | $conditions = [ | |
238 | [ | |
239 | $nearCol, | |
240 | '=', | |
241 | $farCol, | |
242 | ], | |
243 | ]; | |
244 | if ($dynamicCol) { | |
245 | $conditions[] = [ | |
246 | $dynamicCol, | |
247 | '=', | |
248 | "'$targetTable'", | |
249 | ]; | |
250 | } | |
251 | return $conditions; | |
22601c92 CW |
252 | } |
253 | ||
2f616560 CW |
254 | /** |
255 | * @param $alias | |
256 | * @param array ...$entities | |
257 | * @return array | |
258 | */ | |
259 | private static function getJoinDefaults($alias, ...$entities):array { | |
260 | $conditions = []; | |
261 | foreach ($entities as $entity) { | |
262 | foreach ($entity['ui_join_filters'] ?? [] as $fieldName) { | |
263 | $field = civicrm_api4($entity['name'], 'getFields', [ | |
264 | 'select' => ['options'], | |
265 | 'where' => [['name', '=', $fieldName]], | |
266 | 'loadOptions' => ['name'], | |
267 | ])->first(); | |
268 | $value = isset($field['options'][0]) ? json_encode($field['options'][0]['name']) : ''; | |
269 | $conditions[] = [ | |
270 | $alias . '.' . $fieldName . ($value ? ':name' : ''), | |
271 | '=', | |
272 | $value, | |
273 | ]; | |
274 | } | |
275 | } | |
276 | return $conditions; | |
277 | } | |
278 | ||
22601c92 | 279 | } |