Merge pull request #20251 from larssandergreen/change-registration-button-text
[civicrm-core.git] / Civi / Api4 / Generic / BasicGetFieldsAction.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
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 |
10 +--------------------------------------------------------------------+
11 */
12
13 namespace Civi\Api4\Generic;
14
15 use Civi\API\Exception\NotImplementedException;
16
17 /**
18 * Lists information about fields for the $ENTITY entity.
19 *
20 * This field information is also known as "metadata."
21 *
22 * Note that different actions may support different lists of fields.
23 * By default this will fetch the field list relevant to `get`,
24 * but a different list may be returned if you specify another action.
25 *
26 * @method $this setLoadOptions(bool|array $value)
27 * @method bool|array getLoadOptions()
28 * @method $this setAction(string $value)
29 * @method $this setValues(array $values)
30 * @method array getValues()
31 */
32 class BasicGetFieldsAction extends BasicGetAction {
33
34 /**
35 * Fetch option lists for fields?
36 *
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
47 */
48 protected $loadOptions = FALSE;
49
50 /**
51 * Fields will be returned appropriate to the specified action (get, create, delete, etc.)
52 *
53 * @var string
54 */
55 protected $action = 'get';
56
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
64 /**
65 * @var bool
66 * @deprecated
67 */
68 protected $includeCustom;
69
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 {
86 $actionClass = \Civi\API\Request::create($this->getEntityName(), $this->getAction(), ['version' => 4]);
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 }
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);
99 $this->queryArray($values, $result);
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 *
107 * Format option lists.
108 *
109 * In most cases it's not necessary to override this function, even if your entity is really weird.
110 * Instead just override $this->fields and this function will respect that.
111 *
112 * @param array $values
113 * @param bool $isInternal
114 */
115 protected function formatResults(&$values, $isInternal) {
116 $fieldDefaults = array_column($this->fields(), 'default_value', 'name') +
117 array_fill_keys(array_column($this->fields(), 'name'), NULL);
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 }
126 // Unless this is an internal getFields call, filter out @internal properties
127 $internalProps = $isInternal ? [] : array_filter(array_column($this->fields(), '@internal', 'name'));
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(),
132 'options' => !empty($field['pseudoconstant']),
133 ], $fieldDefaults);
134 $field += $defaults + $fieldDefaults;
135 if (array_key_exists('label', $fieldDefaults)) {
136 $field['label'] = $field['label'] ?? $field['title'] ?? $field['name'];
137 }
138 if (!empty($field['options']) && is_array($field['options']) && empty($field['suffixes']) && array_key_exists('suffixes', $field)) {
139 $this->setFieldSuffixes($field);
140 }
141 if (isset($defaults['options'])) {
142 $field['options'] = $this->formatOptionList($field['options']);
143 }
144 $field = array_diff_key($field, $internalProps);
145 }
146 }
147
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,
180 'name' => $option,
181 'label' => $option,
182 ];
183 }
184 $formatted[] = array_intersect_key($option, array_flip($this->loadOptions));
185 }
186 return $formatted;
187 }
188
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
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
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
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
246 public function fields() {
247 return [
248 [
249 'name' => 'name',
250 'data_type' => 'String',
251 'description' => ts('Unique field identifier'),
252 ],
253 [
254 'name' => 'title',
255 'data_type' => 'String',
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'),
262 ],
263 [
264 'name' => 'description',
265 'data_type' => 'String',
266 'description' => ts('Explanation of the purpose of the field'),
267 ],
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 ],
279 [
280 'name' => 'default_value',
281 'data_type' => 'String',
282 ],
283 [
284 'name' => 'required',
285 'data_type' => 'Boolean',
286 'default_value' => FALSE,
287 ],
288 [
289 'name' => 'required_if',
290 'data_type' => 'String',
291 ],
292 [
293 'name' => 'options',
294 'data_type' => 'Array',
295 'default_value' => FALSE,
296 ],
297 [
298 'name' => 'suffixes',
299 'data_type' => 'Array',
300 'default_value' => NULL,
301 'options' => ['name', 'label', 'description', 'abbr', 'color', 'icon'],
302 'description' => 'Available option transformations, e.g. :name, :label',
303 ],
304 [
305 'name' => 'operators',
306 'data_type' => 'Array',
307 'description' => 'If set, limits the operators that can be used on this field for "get" actions.',
308 ],
309 [
310 'name' => 'data_type',
311 'default_value' => 'String',
312 'options' => [
313 'Array' => ts('Array'),
314 'Boolean' => ts('Boolean'),
315 'Date' => ts('Date'),
316 'Float' => ts('Float'),
317 'Integer' => ts('Integer'),
318 'String' => ts('String'),
319 'Text' => ts('Text'),
320 'Timestamp' => ts('Timestamp'),
321 ],
322 ],
323 [
324 'name' => 'input_type',
325 'data_type' => 'String',
326 'options' => [
327 'ChainSelect' => ts('Chain-Select'),
328 'CheckBox' => ts('Checkboxes'),
329 'Date' => ts('Date Picker'),
330 'EntityRef' => ts('Autocomplete Entity'),
331 'File' => ts('File'),
332 'Number' => ts('Number'),
333 'Radio' => ts('Radio Buttons'),
334 'Select' => ts('Select'),
335 'Text' => ts('Text'),
336 ],
337 ],
338 [
339 'name' => 'input_attrs',
340 'data_type' => 'Array',
341 ],
342 [
343 'name' => 'fk_entity',
344 'data_type' => 'String',
345 ],
346 [
347 'name' => 'serialize',
348 'data_type' => 'Integer',
349 ],
350 [
351 'name' => 'entity',
352 'data_type' => 'String',
353 ],
354 [
355 'name' => 'readonly',
356 'data_type' => 'Boolean',
357 'description' => 'True for auto-increment, calculated, or otherwise non-editable fields.',
358 'default_value' => FALSE,
359 ],
360 [
361 'name' => 'output_formatters',
362 'data_type' => 'Array',
363 '@internal' => TRUE,
364 ],
365 ];
366 }
367
368 }