4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 namespace Civi\Api4\Generic
;
15 use Civi\API\Exception\NotImplementedException
;
18 * Lists information about fields for the $ENTITY entity.
20 * This field information is also known as "metadata."
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.
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()
32 class BasicGetFieldsAction
extends BasicGetAction
{
35 * Fetch option lists for fields?
37 * This parameter can be either a boolean or an array of attributes to return from the option list:
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).
48 protected $loadOptions = FALSE;
51 * Fields will be returned appropriate to the specified action (get, create, delete, etc.)
55 protected $action = 'get';
58 * Fields will be returned appropriate to the specified values (e.g. ['contact_type' => 'Individual'])
62 protected $values = [];
68 protected $includeCustom;
71 * To implement getFields for your own entity:
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().
81 * @param Result $result
82 * @throws \Civi\API\Exception\NotImplementedException
84 public function _run(Result
$result) {
86 $actionClass = \Civi\API\Request
::create($this->getEntityName(), $this->getAction(), ['version' => 4]);
88 catch (NotImplementedException
$e) {
90 if (isset($actionClass) && method_exists($actionClass, 'fields')) {
91 $values = $actionClass->fields();
94 $values = $this->getRecords();
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);
103 * Ensure every result contains, at minimum, the array keys as defined in $this->fields.
105 * Attempt to set some sensible defaults for some fields.
107 * Format option lists.
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.
112 * @param array $values
113 * @param bool $isInternal
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]);
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']),
134 $field +
= $defaults +
$fieldDefaults;
135 if (array_key_exists('label', $fieldDefaults)) {
136 $field['label'] = $field['label'] ??
$field['title'] ??
$field['name'];
138 if (!empty($field['options']) && is_array($field['options']) && empty($field['suffixes']) && array_key_exists('suffixes', $field)) {
139 $this->setFieldSuffixes($field);
141 if (isset($defaults['options'])) {
142 $field['options'] = $this->formatOptionList($field['options']);
144 $field = array_diff_key($field, $internalProps);
149 * Transforms option list into the format specified in $this->loadOptions
154 private function formatOptionList($options) {
155 if (!$this->loadOptions ||
!is_array($options)) {
156 return (bool) $options;
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'];
174 // Non-associative array of multiple properties requested
175 foreach ($options as $id => $option) {
176 // Transform a flat list
177 if (!is_array($option)) {
184 $formatted[] = array_intersect_key($option, array_flip($this->loadOptions
));
190 * Set supported field suffixes based on available option keys
191 * @param array $field
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);
208 public function getAction() {
209 // For actions that build on top of other actions, return fields for the simpler action
212 'replace' => 'create',
214 return $sub[$this->action
] ??
$this->action
;
218 * Add an item to the values array
219 * @param string $fieldName
220 * @param mixed $value
223 public function addValue(string $fieldName, $value) {
224 $this->values
[$fieldName] = $value;
229 * Helper function to retrieve options from an option group (for non-DAO entities).
231 * @param string $optionGroupName
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'];
246 public function fields() {
250 'data_type' => 'String',
251 'description' => ts('Unique field identifier'),
255 'data_type' => 'String',
256 'description' => ts('Technical name of field, shown in API and exports'),
260 'data_type' => 'String',
261 'description' => ts('User-facing label, shown on most forms and displays'),
264 'name' => 'description',
265 'data_type' => 'String',
266 'description' => ts('Explanation of the purpose of the field'),
270 'data_type' => 'String',
271 'default_value' => 'Field',
273 'Field' => ts('Primary Field'),
274 'Custom' => ts('Custom Field'),
275 'Filter' => ts('Search Filter'),
276 'Extra' => ts('Extra API Field'),
280 'name' => 'default_value',
281 'data_type' => 'String',
284 'name' => 'required',
285 'description' => 'Is this field required when creating a new entity',
286 'data_type' => 'Boolean',
287 'default_value' => FALSE,
290 'name' => 'nullable',
291 'description' => 'Whether a null value is allowed in this field',
292 'data_type' => 'Boolean',
293 'default_value' => TRUE,
296 'name' => 'required_if',
297 'data_type' => 'String',
301 'data_type' => 'Array',
302 'default_value' => FALSE,
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',
312 'name' => 'operators',
313 'data_type' => 'Array',
314 'description' => 'If set, limits the operators that can be used on this field for "get" actions.',
317 'name' => 'data_type',
318 'default_value' => 'String',
320 'Array' => ts('Array'),
321 'Boolean' => ts('Boolean'),
322 'Date' => ts('Date'),
323 'Float' => ts('Float'),
324 'Integer' => ts('Integer'),
325 'String' => ts('String'),
326 'Text' => ts('Text'),
327 'Timestamp' => ts('Timestamp'),
331 'name' => 'input_type',
332 'data_type' => 'String',
334 'ChainSelect' => ts('Chain-Select'),
335 'CheckBox' => ts('Checkboxes'),
336 'Date' => ts('Date Picker'),
337 'EntityRef' => ts('Autocomplete Entity'),
338 'File' => ts('File'),
339 'Number' => ts('Number'),
340 'Radio' => ts('Radio Buttons'),
341 'Select' => ts('Select'),
342 'Text' => ts('Text'),
346 'name' => 'input_attrs',
347 'data_type' => 'Array',
350 'name' => 'fk_entity',
351 'data_type' => 'String',
354 'name' => 'serialize',
355 'data_type' => 'Integer',
359 'data_type' => 'String',
362 'name' => 'readonly',
363 'data_type' => 'Boolean',
364 'description' => 'True for auto-increment, calculated, or otherwise non-editable fields.',
365 'default_value' => FALSE,
368 'name' => 'output_formatters',
369 'data_type' => 'Array',