APIv4 - Support pseudoconstant suffixes in getFields
[civicrm-core.git] / Civi / Api4 / Generic / BasicGetFieldsAction.php
CommitLineData
19b53e5b
C
1<?php
2
380f3545
TO
3/*
4 +--------------------------------------------------------------------+
41498ac5 5 | Copyright CiviCRM LLC. All rights reserved. |
380f3545 6 | |
41498ac5
TO
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
380f3545
TO
10 +--------------------------------------------------------------------+
11 */
12
19b53e5b
C
13namespace Civi\Api4\Generic;
14
15use Civi\API\Exception\NotImplementedException;
19b53e5b
C
16
17/**
fc95d9a5 18 * Lists information about fields for the $ENTITY entity.
e15f9453
CW
19 *
20 * This field information is also known as "metadata."
21 *
22 * Note that different actions may support different lists of fields.
fc95d9a5 23 * By default this will fetch the field list relevant to `get`,
e15f9453 24 * but a different list may be returned if you specify another action.
19b53e5b 25 *
bb6bfd68
CW
26 * @method $this setLoadOptions(bool|array $value)
27 * @method bool|array getLoadOptions()
19b53e5b 28 * @method $this setAction(string $value)
c752d94b
CW
29 * @method $this setValues(array $values)
30 * @method array getValues()
19b53e5b
C
31 */
32class BasicGetFieldsAction extends BasicGetAction {
33
34 /**
35 * Fetch option lists for fields?
36 *
bb6bfd68
CW
37 * This parameter can be either a boolean or an array of attributes to return from the option list:
38 *
39 * - If `FALSE`, each field's `options` property will be a boolean indicating whether the field has an option list
40 * - If `TRUE`, `options` will be returned as a flat array of the option list's `[id => label]`
41 * - If an array, `options` will be a non-associative array of requested properties:
42 * id, name, label, abbr, description, color, icon
43 * e.g. `loadOptions: ['id', 'name', 'label']` will return an array like `[[id: 1, name: 'Meeting', label: 'Meeting'], ...]`
44 * (note that names and labels are generally ONLY the same when the site's language is set to English).
45 *
46 * @var bool|array
19b53e5b
C
47 */
48 protected $loadOptions = FALSE;
49
50 /**
c752d94b
CW
51 * Fields will be returned appropriate to the specified action (get, create, delete, etc.)
52 *
19b53e5b
C
53 * @var string
54 */
55 protected $action = 'get';
56
c752d94b
CW
57 /**
58 * Fields will be returned appropriate to the specified values (e.g. ['contact_type' => 'Individual'])
59 *
60 * @var array
61 */
62 protected $values = [];
63
a1415a02
CW
64 /**
65 * @var bool
66 * @deprecated
67 */
68 protected $includeCustom;
69
19b53e5b
C
70 /**
71 * To implement getFields for your own entity:
72 *
73 * 1. From your entity class add a static getFields method.
74 * 2. That method should construct and return this class.
75 * 3. The 3rd argument passed to this constructor should be a function that returns an
76 * array of fields for your entity's CRUD actions.
77 * 4. For non-crud actions that need a different set of fields, you can override the
78 * list from step 3 on a per-action basis by defining a fields() method in that action.
79 * See for example BasicGetFieldsAction::fields() or GetActions::fields().
80 *
81 * @param Result $result
82 * @throws \Civi\API\Exception\NotImplementedException
83 */
84 public function _run(Result $result) {
85 try {
3a8dc228 86 $actionClass = \Civi\API\Request::create($this->getEntityName(), $this->getAction(), ['version' => 4]);
19b53e5b
C
87 }
88 catch (NotImplementedException $e) {
89 }
90 if (isset($actionClass) && method_exists($actionClass, 'fields')) {
91 $values = $actionClass->fields();
92 }
93 else {
94 $values = $this->getRecords();
95 }
4b3b32e5
CW
96 // $isInternal param is not part of function signature (to be compatible with parent class)
97 $isInternal = func_get_args()[1] ?? FALSE;
98 $this->formatResults($values, $isInternal);
651c4c95 99 $this->queryArray($values, $result);
19b53e5b
C
100 }
101
102 /**
103 * Ensure every result contains, at minimum, the array keys as defined in $this->fields.
104 *
105 * Attempt to set some sensible defaults for some fields.
106 *
bb6bfd68
CW
107 * Format option lists.
108 *
19b53e5b 109 * In most cases it's not necessary to override this function, even if your entity is really weird.
bb6bfd68 110 * Instead just override $this->fields and this function will respect that.
19b53e5b
C
111 *
112 * @param array $values
4b3b32e5 113 * @param bool $isInternal
19b53e5b 114 */
4b3b32e5 115 protected function formatResults(&$values, $isInternal) {
88620a59
CW
116 $fieldDefaults = array_column($this->fields(), 'default_value', 'name') +
117 array_fill_keys(array_column($this->fields(), 'name'), NULL);
0d20e81c
CW
118 // Enforce field permissions
119 if ($this->checkPermissions) {
120 foreach ($values as $key => $field) {
121 if (!empty($field['permission']) && !\CRM_Core_Permission::check($field['permission'])) {
122 unset($values[$key]);
123 }
124 }
125 }
4b3b32e5
CW
126 // Unless this is an internal getFields call, filter out @internal properties
127 $internalProps = $isInternal ? [] : array_filter(array_column($this->fields(), '@internal', 'name'));
19b53e5b
C
128 foreach ($values as &$field) {
129 $defaults = array_intersect_key([
130 'title' => empty($field['name']) ? NULL : ucwords(str_replace('_', ' ', $field['name'])),
131 'entity' => $this->getEntityName(),
19b53e5b 132 'options' => !empty($field['pseudoconstant']),
88620a59
CW
133 ], $fieldDefaults);
134 $field += $defaults + $fieldDefaults;
135 if (array_key_exists('label', $fieldDefaults)) {
136 $field['label'] = $field['label'] ?? $field['title'] ?? $field['name'];
137 }
b1b7d409
CW
138 if (!empty($field['options']) && is_array($field['options']) && empty($field['suffixes']) && array_key_exists('suffixes', $field)) {
139 $this->setFieldSuffixes($field);
140 }
bb6bfd68
CW
141 if (isset($defaults['options'])) {
142 $field['options'] = $this->formatOptionList($field['options']);
19b53e5b 143 }
4b3b32e5 144 $field = array_diff_key($field, $internalProps);
19b53e5b
C
145 }
146 }
147
bb6bfd68
CW
148 /**
149 * Transforms option list into the format specified in $this->loadOptions
150 *
151 * @param $options
152 * @return array|bool
153 */
154 private function formatOptionList($options) {
155 if (!$this->loadOptions || !is_array($options)) {
156 return (bool) $options;
157 }
158 if (!$options) {
159 return $options;
160 }
161 $formatted = [];
162 $first = reset($options);
163 // Flat array requested
164 if ($this->loadOptions === TRUE) {
165 // Convert non-associative to flat array
166 if (is_array($first) && isset($first['id'])) {
167 foreach ($options as $option) {
168 $formatted[$option['id']] = $option['label'] ?? $option['name'] ?? $option['id'];
169 }
170 return $formatted;
171 }
172 return $options;
173 }
174 // Non-associative array of multiple properties requested
175 foreach ($options as $id => $option) {
176 // Transform a flat list
177 if (!is_array($option)) {
178 $option = [
179 'id' => $id,
79e6ec68 180 'name' => $id,
bb6bfd68
CW
181 'label' => $option,
182 ];
183 }
184 $formatted[] = array_intersect_key($option, array_flip($this->loadOptions));
185 }
186 return $formatted;
187 }
188
b1b7d409
CW
189 /**
190 * Set supported field suffixes based on available option keys
191 * @param array $field
192 */
193 private function setFieldSuffixes(array &$field) {
194 // These suffixes are always supported if a field has options
195 $field['suffixes'] = ['name', 'label'];
196 $firstOption = reset($field['options']);
197 // If first option is an array, merge in those keys as available suffixes
198 if (is_array($firstOption)) {
199 // Remove 'id' because there is no practical reason to use it as a field suffix
200 $otherKeys = array_diff(array_keys($firstOption), ['id', 'name', 'label']);
201 $field['suffixes'] = array_merge($field['suffixes'], $otherKeys);
202 }
203 }
204
19b53e5b
C
205 /**
206 * @return string
207 */
208 public function getAction() {
209 // For actions that build on top of other actions, return fields for the simpler action
210 $sub = [
211 'save' => 'create',
212 'replace' => 'create',
213 ];
214 return $sub[$this->action] ?? $this->action;
215 }
216
121ec912
CW
217 /**
218 * Add an item to the values array
219 * @param string $fieldName
220 * @param mixed $value
221 * @return $this
222 */
223 public function addValue(string $fieldName, $value) {
224 $this->values[$fieldName] = $value;
225 return $this;
226 }
227
4f664e9c
CW
228 /**
229 * Helper function to retrieve options from an option group (for non-DAO entities).
230 *
231 * @param string $optionGroupName
232 */
233 public function pseudoconstantOptions(string $optionGroupName) {
234 if ($this->getLoadOptions()) {
235 $options = \CRM_Core_OptionValue::getValues(['name' => $optionGroupName]);
236 foreach ($options as &$option) {
237 $option['id'] = $option['value'];
238 }
239 }
240 else {
241 $options = TRUE;
242 }
243 return $options;
244 }
245
19b53e5b
C
246 public function fields() {
247 return [
248 [
249 'name' => 'name',
250 'data_type' => 'String',
b6b6cb2d 251 'description' => ts('Unique field identifier'),
19b53e5b
C
252 ],
253 [
254 'name' => 'title',
255 'data_type' => 'String',
b6b6cb2d
CW
256 'description' => ts('Technical name of field, shown in API and exports'),
257 ],
258 [
259 'name' => 'label',
260 'data_type' => 'String',
261 'description' => ts('User-facing label, shown on most forms and displays'),
19b53e5b
C
262 ],
263 [
264 'name' => 'description',
265 'data_type' => 'String',
b6b6cb2d 266 'description' => ts('Explanation of the purpose of the field'),
19b53e5b 267 ],
a1415a02
CW
268 [
269 'name' => 'type',
270 'data_type' => 'String',
271 'default_value' => 'Field',
272 'options' => [
273 'Field' => ts('Primary Field'),
274 'Custom' => ts('Custom Field'),
275 'Filter' => ts('Search Filter'),
276 'Extra' => ts('Extra API Field'),
277 ],
278 ],
19b53e5b
C
279 [
280 'name' => 'default_value',
281 'data_type' => 'String',
282 ],
283 [
284 'name' => 'required',
f8d39baf 285 'description' => 'Is this field required when creating a new entity',
19b53e5b 286 'data_type' => 'Boolean',
88620a59 287 'default_value' => FALSE,
19b53e5b 288 ],
f8d39baf
CW
289 [
290 'name' => 'nullable',
291 'description' => 'Whether a null value is allowed in this field',
292 'data_type' => 'Boolean',
293 'default_value' => TRUE,
294 ],
19b53e5b
C
295 [
296 'name' => 'required_if',
297 'data_type' => 'String',
298 ],
299 [
300 'name' => 'options',
301 'data_type' => 'Array',
88620a59 302 'default_value' => FALSE,
19b53e5b 303 ],
b1b7d409
CW
304 [
305 'name' => 'suffixes',
306 'data_type' => 'Array',
307 'default_value' => NULL,
308 'options' => ['name', 'label', 'description', 'abbr', 'color', 'icon'],
309 'description' => 'Available option transformations, e.g. :name, :label',
310 ],
a1415a02
CW
311 [
312 'name' => 'operators',
313 'data_type' => 'Array',
314 'description' => 'If set, limits the operators that can be used on this field for "get" actions.',
315 ],
19b53e5b
C
316 [
317 'name' => 'data_type',
88620a59 318 'default_value' => 'String',
de2b4328 319 'options' => [
c0e68893 320 'Array' => ts('Array'),
de2b4328 321 'Boolean' => ts('Boolean'),
c0e68893 322 'Date' => ts('Date'),
323 'Float' => ts('Float'),
324 'Integer' => ts('Integer'),
de2b4328
CW
325 'String' => ts('String'),
326 'Text' => ts('Text'),
de2b4328 327 'Timestamp' => ts('Timestamp'),
de2b4328 328 ],
19b53e5b
C
329 ],
330 [
331 'name' => 'input_type',
332 'data_type' => 'String',
de2b4328 333 'options' => [
783a2874
CW
334 'ChainSelect' => ts('Chain-Select'),
335 'CheckBox' => ts('Checkboxes'),
336 'Date' => ts('Date Picker'),
337 'EntityRef' => ts('Autocomplete Entity'),
c0e68893 338 'File' => ts('File'),
339 'Number' => ts('Number'),
783a2874 340 'Radio' => ts('Radio Buttons'),
c0e68893 341 'Select' => ts('Select'),
342 'Text' => ts('Text'),
de2b4328 343 ],
19b53e5b
C
344 ],
345 [
346 'name' => 'input_attrs',
347 'data_type' => 'Array',
348 ],
349 [
350 'name' => 'fk_entity',
351 'data_type' => 'String',
352 ],
353 [
354 'name' => 'serialize',
355 'data_type' => 'Integer',
356 ],
357 [
358 'name' => 'entity',
359 'data_type' => 'String',
360 ],
34745448
CW
361 [
362 'name' => 'readonly',
363 'data_type' => 'Boolean',
6fb85546 364 'description' => 'True for auto-increment, calculated, or otherwise non-editable fields.',
88620a59 365 'default_value' => FALSE,
34745448 366 ],
96f09dda
CW
367 [
368 'name' => 'output_formatters',
369 'data_type' => 'Array',
4b3b32e5 370 '@internal' => TRUE,
96f09dda 371 ],
19b53e5b
C
372 ];
373 }
374
375}