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\Service\Spec
;
15 use Civi\Api4\Utils\CoreUtil
;
16 use CRM_Core_DAO_AllCoreTables
as AllCoreTables
;
22 * @param string $entity
26 public static function arrayToField(array $data, $entity) {
27 $dataTypeName = self
::getDataType($data);
29 if (!empty($data['custom_group_id'])) {
30 $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName);
31 if (strpos($entity, 'Custom_') !== 0) {
32 $field->setName($data['custom_group_id.name'] . '.' . $data['name']);
35 $field->setTableName($data['custom_group_id.table_name']);
37 $field->setColumnName($data['column_name']);
38 $field->setCustomFieldId($data['id'] ??
NULL);
39 $field->setCustomGroupName($data['custom_group_id.name']);
40 $field->setTitle($data['label']);
41 $field->setLabel($data['custom_group_id.title'] . ': ' . $data['label']);
42 $field->setHelpPre($data['help_pre'] ??
NULL);
43 $field->setHelpPost($data['help_post'] ??
NULL);
44 if (self
::customFieldHasOptions($data)) {
45 $field->setOptionsCallback([__CLASS__
, 'getOptions']);
46 if (!empty($data['option_group_id'])) {
47 // Option groups support other stuff like description, icon & color,
48 // but at time of this writing, custom fields do not.
49 $field->setSuffixes(['id', 'name', 'label']);
52 $field->setReadonly($data['is_view']);
55 $name = $data['name'] ??
NULL;
56 $field = new FieldSpec($name, $entity, $dataTypeName);
57 $field->setType('Field');
58 $field->setColumnName($name);
59 $field->setRequired(!empty($data['required']));
60 $field->setTitle($data['title'] ??
NULL);
61 $field->setLabel($data['html']['label'] ??
NULL);
62 if (!empty($data['pseudoconstant'])) {
63 // Do not load options if 'prefetch' is explicitly FALSE
64 if (!isset($data['pseudoconstant']['prefetch']) ||
$data['pseudoconstant']['prefetch'] === FALSE) {
65 $field->setOptionsCallback([__CLASS__
, 'getOptions']);
67 // These suffixes are always supported if a field has options
68 $suffixes = ['name', 'label'];
69 // Add other columns specified in schema (e.g. 'abbrColumn')
70 foreach (['description', 'abbr', 'icon', 'color'] as $suffix) {
71 if (isset($data['pseudoconstant'][$suffix . 'Column'])) {
72 $suffixes[] = $suffix;
75 $field->setSuffixes($suffixes);
77 $field->setReadonly(!empty($data['readonly']));
79 $field->setSerialize($data['serialize'] ??
NULL);
80 $field->setDefaultValue($data['default'] ??
NULL);
81 $field->setDescription($data['description'] ??
NULL);
82 self
::setInputTypeAndAttrs($field, $data, $dataTypeName);
84 $field->setPermission($data['permission'] ??
NULL);
85 $fkAPIName = $data['FKApiName'] ??
NULL;
86 $fkClassName = $data['FKClassName'] ??
NULL;
87 if ($fkAPIName ||
$fkClassName) {
88 $field->setFkEntity($fkAPIName ?
: AllCoreTables
::getBriefName($fkClassName));
95 * Does this custom field have options
100 private static function customFieldHasOptions($field) {
101 // This will include boolean fields with Yes/No options.
102 if (in_array($field['html_type'], ['Radio', 'CheckBox'])) {
105 // Do this before the "Select" string search because date fields have a "Select Date" html_type
106 // and contactRef fields have an "Autocomplete-Select" html_type - contacts are an FK not an option list.
107 if (in_array($field['data_type'], ['ContactReference', 'Date'])) {
110 if (strpos($field['html_type'], 'Select') !== FALSE) {
113 return !empty($field['option_group_id']);
117 * Get the data type from an array. Defaults to 'data_type' with fallback to
118 * mapping for the integer value 'type'
124 private static function getDataType(array $data) {
125 if (isset($data['data_type'])) {
126 return !empty($data['time_format']) ?
'Timestamp' : $data['data_type'];
129 $dataTypeInt = $data['type'] ??
NULL;
130 $dataTypeName = \CRM_Utils_Type
::typeToString($dataTypeInt);
132 return $dataTypeName;
136 * Callback function to build option lists for all DAO & custom fields.
138 * @param FieldSpec $spec
139 * @param array $values
140 * @param bool|array $returnFormat
141 * @param bool $checkPermissions
142 * @return array|false
144 public static function getOptions($spec, $values, $returnFormat, $checkPermissions) {
145 $fieldName = $spec->getName();
147 if ($spec instanceof CustomFieldSpec
) {
148 // buildOptions relies on the custom_* type of field names
149 $fieldName = sprintf('custom_%d', $spec->getCustomFieldId());
152 // BAO::buildOptions returns a single-dimensional list, we call that first because of the hook contract,
153 // @see CRM_Utils_Hook::fieldOptions
154 // We then supplement the data with additional properties if requested.
155 $bao = CoreUtil
::getBAOFromApiName($spec->getEntity());
156 $optionLabels = $bao::buildOptions($fieldName, NULL, $values);
158 if (!is_array($optionLabels) ||
!$optionLabels) {
162 $options = \CRM_Utils_Array
::makeNonAssociative($optionLabels, 'id', 'label');
163 if (is_array($returnFormat)) {
164 self
::addOptionProps($options, $spec, $bao, $fieldName, $values, $returnFormat);
171 * Augment the 2 values returned by BAO::buildOptions (id, label) with extra properties (name, description, color, icon, etc).
173 * We start with BAO::buildOptions in order to respect hooks which may be adding/removing items, then we add the extra data.
175 * @param array $options
176 * @param FieldSpec $spec
177 * @param \CRM_Core_DAO $baoName
178 * @param string $fieldName
179 * @param array $values
180 * @param array $returnFormat
182 private static function addOptionProps(&$options, $spec, $baoName, $fieldName, $values, $returnFormat) {
183 // FIXME: For now, call the buildOptions function again and then combine the arrays. Not an ideal approach.
184 // TODO: Teach CRM_Core_Pseudoconstant to always load multidimensional option lists so we can get more properties like 'color' and 'icon',
185 // however that might require a change to the hook_civicrm_fieldOptions signature so that's a bit tricky.
186 if (in_array('name', $returnFormat)) {
187 $props['name'] = $baoName::buildOptions($fieldName, 'validate', $values);
189 $returnFormat = array_diff($returnFormat, ['id', 'name', 'label']);
190 // CRM_Core_Pseudoconstant doesn't know how to fetch extra stuff like icon, description, color, etc., so we have to invent that wheel here...
192 $optionIds = implode(',', array_column($options, 'id'));
193 $optionIndex = array_flip(array_column($options, 'id'));
194 if ($spec instanceof CustomFieldSpec
) {
195 $optionGroupId = \CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_CustomField', $spec->getCustomFieldId(), 'option_group_id');
198 $dao = new $baoName();
199 $fieldSpec = $dao->getFieldSpec($fieldName);
200 $pseudoconstant = $fieldSpec['pseudoconstant'] ??
NULL;
201 $optionGroupName = $pseudoconstant['optionGroupName'] ??
NULL;
202 $optionGroupId = $optionGroupName ? \CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroupName, 'id', 'name') : NULL;
204 if (!empty($optionGroupId)) {
205 $extraStuff = \CRM_Core_BAO_OptionValue
::getOptionValuesArray($optionGroupId);
206 $keyColumn = $pseudoconstant['keyColumn'] ??
'value';
207 foreach ($extraStuff as $item) {
208 if (isset($optionIndex[$item[$keyColumn]])) {
209 foreach ($returnFormat as $ret) {
210 // Note: our schema is inconsistent about whether `description` fields allow html,
211 // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
212 $options[$optionIndex[$item[$keyColumn]]][$ret] = ($ret === 'description' && isset($item[$ret])) ?
strip_tags($item[$ret]) : $item[$ret] ??
NULL;
218 // Fetch the abbr if requested using context: abbreviate
219 if (in_array('abbr', $returnFormat)) {
220 $props['abbr'] = $baoName::buildOptions($fieldName, 'abbreviate', $values);
221 $returnFormat = array_diff($returnFormat, ['abbr']);
223 // Fetch anything else (color, icon, description)
224 if ($returnFormat && !empty($pseudoconstant['table']) && \CRM_Utils_Rule
::commaSeparatedIntegers($optionIds)) {
225 $sql = "SELECT * FROM {$pseudoconstant['table']} WHERE id IN (%1)";
226 $query = \CRM_Core_DAO
::executeQuery($sql, [1 => [$optionIds, 'CommaSeparatedIntegers']]);
227 while ($query->fetch()) {
228 foreach ($returnFormat as $ret) {
229 if (property_exists($query, $ret)) {
230 // Note: our schema is inconsistent about whether `description` fields allow html,
231 // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
232 $options[$optionIndex[$query->id
]][$ret] = $ret === 'description' ?
strip_tags($query->$ret) : $query->$ret;
240 foreach ($options as &$option) {
241 foreach ($props as $name => $prop) {
242 $option[$name] = $prop[$option['id']] ??
NULL;
249 * @param \Civi\Api4\Service\Spec\FieldSpec $fieldSpec
251 * @param string $dataTypeName
253 public static function setInputTypeAndAttrs(FieldSpec
&$fieldSpec, $data, $dataTypeName) {
254 $inputType = $data['html']['type'] ??
$data['html_type'] ??
NULL;
255 $inputAttrs = $data['html'] ??
[];
256 unset($inputAttrs['type']);
259 'Select Date' => 'Date',
262 $inputType = $map[$inputType] ??
$inputType;
263 if ($dataTypeName === 'ContactReference') {
264 $inputType = 'EntityRef';
266 if (in_array($inputType, ['Select', 'EntityRef'], TRUE) && !empty($data['serialize'])) {
267 $inputAttrs['multiple'] = TRUE;
269 if ($inputType == 'Date' && !empty($inputAttrs['formatType'])) {
270 self
::setLegacyDateFormat($inputAttrs);
272 // Date/time settings from custom fields
273 if ($inputType == 'Date' && !empty($data['custom_group_id'])) {
274 $inputAttrs['time'] = empty($data['time_format']) ?
FALSE : ($data['time_format'] == 1 ?
12 : 24);
275 $inputAttrs['date'] = $data['date_format'];
276 $inputAttrs['start_date_years'] = (int) $data['start_date_years'];
277 $inputAttrs['end_date_years'] = (int) $data['end_date_years'];
279 if ($inputType == 'Text' && !empty($data['maxlength'])) {
280 $inputAttrs['maxlength'] = (int) $data['maxlength'];
282 if ($inputType == 'TextArea') {
283 foreach (['rows', 'cols', 'note_rows', 'note_cols'] as $prop) {
284 if (!empty($data[$prop])) {
285 $inputAttrs[str_replace('note_', '', $prop)] = (int) $data[$prop];
289 // Ensure all keys use lower_case not camelCase
290 foreach ($inputAttrs as $key => $val) {
291 if ($key !== strtolower($key)) {
292 unset($inputAttrs[$key]);
293 $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key));
294 $inputAttrs[$key] = $val;
298 ->setInputType($inputType)
299 ->setInputAttrs($inputAttrs);
303 * @param array $inputAttrs
305 private static function setLegacyDateFormat(&$inputAttrs) {
306 if (empty(\Civi
::$statics['legacyDatePrefs'][$inputAttrs['formatType']])) {
307 \Civi
::$statics['legacyDatePrefs'][$inputAttrs['formatType']] = [];
308 $params = ['name' => $inputAttrs['formatType']];
309 \CRM_Core_DAO
::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, \Civi
::$statics['legacyDatePrefs'][$inputAttrs['formatType']]);
311 $dateFormat = \Civi
::$statics['legacyDatePrefs'][$inputAttrs['formatType']];
312 unset($inputAttrs['formatType']);
313 $inputAttrs['time'] = !empty($dateFormat['time_format']);
314 $inputAttrs['date'] = TRUE;
315 $inputAttrs['start_date_years'] = (int) $dateFormat['start'];
316 $inputAttrs['end_date_years'] = (int) $dateFormat['end'];