Ensure all api3 functions have documented types
[civicrm-core.git] / api / v3 / Generic / Getlist.php
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 /**
13 * @package CiviCRM_APIv3
14 */
15
16 /**
17 * Generic api wrapper used for quicksearch and autocomplete.
18 *
19 * @param array $apiRequest
20 *
21 * @return mixed
22 * @throws \CiviCRM_API3_Exception
23 */
24 function civicrm_api3_generic_getList($apiRequest) {
25 $entity = CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($apiRequest['entity']);
26 $request = $apiRequest['params'];
27 $meta = civicrm_api3_generic_getfields(['action' => 'get'] + $apiRequest, FALSE)['values'];
28
29 // If the user types an integer into the search
30 $forceIdSearch = empty($request['id']) && !empty($request['input']) && !empty($meta['id']) && CRM_Utils_Rule::positiveInteger($request['input']);
31 // Add an extra page of results for the record with an exact id match
32 if ($forceIdSearch) {
33 $request['page_num'] = ($request['page_num'] ?? 1) - 1;
34 if (empty($request['page_num'])) {
35 $request['id'] = $request['input'];
36 unset($request['input']);
37 }
38 }
39
40 // Hey api, would you like to provide default values?
41 $fnName = "_civicrm_api3_{$entity}_getlist_defaults";
42 $defaults = function_exists($fnName) ? $fnName($request) : [];
43 _civicrm_api3_generic_getList_defaults($entity, $request, $defaults, $meta);
44
45 // Hey api, would you like to format the search params?
46 $fnName = "_civicrm_api3_{$entity}_getlist_params";
47 $fnName = function_exists($fnName) ? $fnName : '_civicrm_api3_generic_getlist_params';
48 $fnName($request);
49
50 $request['params']['check_permissions'] = !empty($apiRequest['params']['check_permissions']);
51 $result = civicrm_api3($entity, 'get', $request['params']);
52 if (!empty($request['input']) && !empty($defaults['search_field_fallback']) && $result['count'] < $request['params']['options']['limit']) {
53 // We support a field fallback. Note we don't do this as an OR query because that could easily
54 // bypass an index & kill the server. We just 'pad' the results if needed with the second
55 // query - this is effectively the same as what the old Ajax::getContactEmail function did.
56 // Since these queries should be quick & often only one should be needed this is a simpler alternative
57 // to constructing a UNION via the api.
58 $request['params'][$defaults['search_field_fallback']] = $request['params'][$defaults['search_field']];
59 if ($request['params']['options']['sort'] === $defaults['search_field']) {
60 // The way indexing works here is that the order by field will be chosen in preference to the
61 // filter field. This can result in really bad performance so use the filter field for the sort.
62 // See https://github.com/civicrm/civicrm-core/pull/16993 for performance test results.
63 $request['params']['options']['sort'] = $defaults['search_field_fallback'];
64 }
65 // Exclude anything returned from the previous query since we are looking for additional rows in this
66 // second query.
67 $request['params'][$defaults['search_field']] = ['NOT LIKE' => $request['params'][$defaults['search_field_fallback']]['LIKE']];
68 $request['params']['options']['limit'] -= $result['count'];
69 $result2 = civicrm_api3($entity, 'get', $request['params']);
70 $result['values'] = array_merge($result['values'], $result2['values']);
71 $result['count'] = count($result['values']);
72 }
73 else {
74 // Re-index to sequential = 0.
75 $result['values'] = array_merge($result['values']);
76 }
77
78 // Hey api, would you like to format the output?
79 $fnName = "_civicrm_api3_{$entity}_getlist_output";
80 $fnName = function_exists($fnName) ? $fnName : '_civicrm_api3_generic_getlist_output';
81 $values = $fnName($result, $request, $entity, $meta);
82
83 _civicrm_api3_generic_getlist_postprocess($result, $request, $values);
84
85 $output = ['page_num' => $request['page_num']];
86
87 if ($forceIdSearch) {
88 $output['page_num']++;
89 // When returning the single record matching id
90 if (empty($request['page_num'])) {
91 $output['more_results'] = TRUE;
92 foreach ($values as $i => $value) {
93 $description = ts('ID: %1', [1 => $value['id']]);
94 $values[$i]['description'] = array_merge([$description], $value['description'] ?? []);
95 }
96 }
97 }
98 // Limit is set for searching but not fetching by id
99 elseif (!empty($request['params']['options']['limit'])) {
100 // If we have an extra result then this is not the last page
101 $last = $request['params']['options']['limit'] - 1;
102 $output['more_results'] = isset($values[$last]);
103 unset($values[$last]);
104 }
105
106 return civicrm_api3_create_success($values, $request['params'], $entity, 'getlist', CRM_Core_DAO::$_nullObject, $output);
107 }
108
109 /**
110 * Set defaults for api.getlist.
111 *
112 * @param string $entity
113 * @param array $request
114 * @param array $apiDefaults
115 * @param array $fields
116 */
117 function _civicrm_api3_generic_getList_defaults($entity, &$request, $apiDefaults, $fields) {
118 $defaults = [
119 'page_num' => 1,
120 'input' => '',
121 'image_field' => NULL,
122 'color_field' => isset($fields['color']) ? 'color' : NULL,
123 'id_field' => $entity == 'option_value' ? 'value' : 'id',
124 'description_field' => [],
125 'add_wildcard' => Civi::settings()->get('includeWildCardInName'),
126 'params' => [],
127 'extra' => [],
128 ];
129 // Find main field from meta
130 foreach (['sort_name', 'title', 'label', 'name', 'subject'] as $field) {
131 if (isset($fields[$field])) {
132 $defaults['label_field'] = $defaults['search_field'] = $field;
133 break;
134 }
135 }
136 // Find fields to be used for the description
137 foreach (['description'] as $field) {
138 if (isset($fields[$field])) {
139 $defaults['description_field'][] = $field;
140 }
141 }
142 $resultsPerPage = Civi::settings()->get('search_autocomplete_count');
143 if (isset($request['params']) && isset($apiDefaults['params'])) {
144 $request['params'] += $apiDefaults['params'];
145 }
146 $request += $apiDefaults + $defaults;
147 // Default api params
148 $params = [
149 'sequential' => 0,
150 'options' => [],
151 ];
152 // When searching e.g. autocomplete
153 if ($request['input']) {
154 $params[$request['search_field']] = ['LIKE' => ($request['add_wildcard'] ? '%' : '') . $request['input'] . '%'];
155 }
156 $request['params'] += $params;
157
158 // When looking up a field e.g. displaying existing record
159 if (!empty($request['id'])) {
160 if (is_string($request['id']) && strpos($request['id'], ',')) {
161 $request['id'] = explode(',', trim($request['id'], ', '));
162 }
163 // Don't run into search limits when prefilling selection
164 $request['params']['options']['limit'] = NULL;
165 unset($request['params']['options']['offset']);
166 $request['params'][$request['id_field']] = is_array($request['id']) ? ['IN' => $request['id']] : $request['id'];
167 }
168 else {
169 $request['params']['options'] += [
170 // Add pagination parameters
171 'sort' => $request['label_field'],
172 // Adding one extra result allows us to see if there are any more
173 'limit' => $resultsPerPage + 1,
174 // Because sql is zero-based
175 'offset' => ($request['page_num'] - 1) * $resultsPerPage,
176 ];
177 }
178 }
179
180 /**
181 * Fallback implementation of getlist_params. May be overridden by individual apis.
182 *
183 * @param array $request
184 */
185 function _civicrm_api3_generic_getlist_params(&$request) {
186 $fieldsToReturn = [$request['id_field'], $request['label_field']];
187 if (!empty($request['image_field'])) {
188 $fieldsToReturn[] = $request['image_field'];
189 }
190 if (!empty($request['color_field'])) {
191 $fieldsToReturn[] = $request['color_field'];
192 }
193 if (!empty($request['description_field'])) {
194 $fieldsToReturn = array_merge($fieldsToReturn, (array) $request['description_field']);
195 }
196 $request['params']['return'] = array_unique(array_merge($fieldsToReturn, $request['extra']));
197 }
198
199 /**
200 * Fallback implementation of getlist_output. May be overridden by individual api functions.
201 *
202 * @param array $result
203 * @param array $request
204 * @param string $entity
205 * @param array $fields
206 *
207 * @return array
208 */
209 function _civicrm_api3_generic_getlist_output($result, $request, $entity, $fields) {
210 $output = [];
211 if (!empty($result['values'])) {
212 foreach ($result['values'] as $row) {
213 $data = [
214 'id' => $row[$request['id_field']],
215 'label' => $row[$request['label_field']],
216 ];
217 if (!empty($request['description_field'])) {
218 $data['description'] = [];
219 foreach ((array) $request['description_field'] as $field) {
220 if (!empty($row[$field])) {
221 if (!isset($fields[$field]['pseudoconstant'])) {
222 $data['description'][] = $row[$field];
223 }
224 else {
225 $data['description'][] = CRM_Core_PseudoConstant::getLabel(
226 _civicrm_api3_get_BAO($entity),
227 $field,
228 $row[$field]
229 );
230 }
231 }
232 }
233 }
234 if (!empty($request['image_field'])) {
235 $data['image'] = $row[$request['image_field']] ?? '';
236 }
237 if (isset($row[$request['color_field']])) {
238 $data['color'] = $row[$request['color_field']];
239 }
240 $output[] = $data;
241 }
242 }
243 return $output;
244 }
245
246 /**
247 * Common postprocess for getlist output
248 *
249 * @param array $result
250 * @param array $request
251 * @param array $values
252 */
253 function _civicrm_api3_generic_getlist_postprocess($result, $request, &$values) {
254 $chains = [];
255 foreach ($request['params'] as $field => $param) {
256 if (substr($field, 0, 4) === 'api.') {
257 $chains[] = $field;
258 }
259 }
260 if (!empty($result['values'])) {
261 foreach (array_values($result['values']) as $num => $row) {
262 foreach ($request['extra'] as $field) {
263 $values[$num]['extra'][$field] = $row[$field] ?? NULL;
264 }
265 foreach ($chains as $chain) {
266 $values[$num][$chain] = $row[$chain] ?? NULL;
267 }
268 }
269 }
270 }
271
272 /**
273 * Provide metadata for this api
274 *
275 * @param array $params
276 * @param array $apiRequest
277 */
278 function _civicrm_api3_generic_getlist_spec(&$params, $apiRequest) {
279 $params += [
280 'page_num' => [
281 'title' => 'Page Number',
282 'description' => "Current page of a multi-page lookup",
283 'type' => CRM_Utils_Type::T_INT,
284 ],
285 'input' => [
286 'title' => 'Search Input',
287 'description' => "String to search on",
288 'type' => CRM_Utils_Type::T_TEXT,
289 ],
290 'params' => [
291 'title' => 'API Params',
292 'description' => "Additional filters to send to the {$apiRequest['entity']} API.",
293 ],
294 'extra' => [
295 'title' => 'Extra',
296 'description' => 'Array of additional fields to return.',
297 ],
298 'image_field' => [
299 'title' => 'Image Field',
300 'description' => "Field that this entity uses to store icons (usually automatic)",
301 'type' => CRM_Utils_Type::T_TEXT,
302 ],
303 'id_field' => [
304 'title' => 'ID Field',
305 'description' => "Field that uniquely identifies this entity (usually automatic)",
306 'type' => CRM_Utils_Type::T_TEXT,
307 ],
308 'description_field' => [
309 'title' => 'Description Field',
310 'description' => "Field that this entity uses to store summary text (usually automatic)",
311 'type' => CRM_Utils_Type::T_TEXT,
312 ],
313 'label_field' => [
314 'title' => 'Label Field',
315 'description' => "Field to display as title of results (usually automatic)",
316 'type' => CRM_Utils_Type::T_TEXT,
317 ],
318 'search_field' => [
319 'title' => 'Search Field',
320 'description' => "Field to search on (assumed to be the same as label field unless otherwise specified)",
321 'type' => CRM_Utils_Type::T_TEXT,
322 ],
323 ];
324 }