Commit | Line | Data |
---|---|---|
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 |
13 | namespace Civi\Api4\Service\Spec; |
14 | ||
2bf220fd | 15 | use Civi\Api4\Utils\CoreUtil; |
19b53e5b C |
16 | use CRM_Core_DAO_AllCoreTables as AllCoreTables; |
17 | ||
18 | class SpecFormatter { | |
19 | ||
19b53e5b C |
20 | /** |
21 | * @param array $data | |
22 | * @param string $entity | |
23 | * | |
24 | * @return FieldSpec | |
25 | */ | |
26 | public static function arrayToField(array $data, $entity) { | |
27 | $dataTypeName = self::getDataType($data); | |
28 | ||
29 | if (!empty($data['custom_group_id'])) { | |
30 | $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName); | |
31 | if (strpos($entity, 'Custom_') !== 0) { | |
84ad7693 | 32 | $field->setName($data['custom_group_id.name'] . '.' . $data['name']); |
19b53e5b C |
33 | } |
34 | else { | |
84ad7693 | 35 | $field->setTableName($data['custom_group_id.table_name']); |
19b53e5b | 36 | } |
a689294c | 37 | $field->setColumnName($data['column_name']); |
1b8e3bc8 | 38 | $field->setCustomFieldId($data['id'] ?? NULL); |
84ad7693 | 39 | $field->setCustomGroupName($data['custom_group_id.name']); |
b751bdc9 | 40 | $field->setTitle($data['label']); |
84ad7693 | 41 | $field->setLabel($data['custom_group_id.title'] . ': ' . $data['label']); |
1b8e3bc8 CW |
42 | $field->setHelpPre($data['help_pre'] ?? NULL); |
43 | $field->setHelpPost($data['help_post'] ?? NULL); | |
2bf220fd CW |
44 | if (self::customFieldHasOptions($data)) { |
45 | $field->setOptionsCallback([__CLASS__, 'getOptions']); | |
b1b7d409 CW |
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']); | |
50 | } | |
2bf220fd | 51 | } |
4b3b32e5 | 52 | $field->setReadonly($data['is_view']); |
19b53e5b C |
53 | } |
54 | else { | |
1b8e3bc8 | 55 | $name = $data['name'] ?? NULL; |
19b53e5b | 56 | $field = new FieldSpec($name, $entity, $dataTypeName); |
a1415a02 CW |
57 | $field->setType('Field'); |
58 | $field->setColumnName($name); | |
1b8e3bc8 CW |
59 | $field->setRequired(!empty($data['required'])); |
60 | $field->setTitle($data['title'] ?? NULL); | |
b6b6cb2d | 61 | $field->setLabel($data['html']['label'] ?? NULL); |
2bf220fd | 62 | if (!empty($data['pseudoconstant'])) { |
b1b7d409 | 63 | // Do not load options if 'prefetch' is explicitly FALSE |
3d9604f0 | 64 | if (!isset($data['pseudoconstant']['prefetch']) || $data['pseudoconstant']['prefetch'] === FALSE) { |
b1b7d409 CW |
65 | $field->setOptionsCallback([__CLASS__, 'getOptions']); |
66 | } | |
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; | |
73 | } | |
74 | } | |
75 | $field->setSuffixes($suffixes); | |
2bf220fd | 76 | } |
4b3b32e5 | 77 | $field->setReadonly(!empty($data['readonly'])); |
19b53e5b | 78 | } |
08a06697 | 79 | $field->setSerialize($data['serialize'] ?? NULL); |
1b8e3bc8 CW |
80 | $field->setDefaultValue($data['default'] ?? NULL); |
81 | $field->setDescription($data['description'] ?? NULL); | |
19b53e5b C |
82 | self::setInputTypeAndAttrs($field, $data, $dataTypeName); |
83 | ||
1b8e3bc8 CW |
84 | $field->setPermission($data['permission'] ?? NULL); |
85 | $fkAPIName = $data['FKApiName'] ?? NULL; | |
86 | $fkClassName = $data['FKClassName'] ?? NULL; | |
19b53e5b C |
87 | if ($fkAPIName || $fkClassName) { |
88 | $field->setFkEntity($fkAPIName ?: AllCoreTables::getBriefName($fkClassName)); | |
89 | } | |
90 | ||
91 | return $field; | |
92 | } | |
93 | ||
94 | /** | |
95 | * Does this custom field have options | |
96 | * | |
97 | * @param array $field | |
98 | * @return bool | |
99 | */ | |
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'])) { | |
103 | return TRUE; | |
104 | } | |
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'])) { | |
108 | return FALSE; | |
109 | } | |
110 | if (strpos($field['html_type'], 'Select') !== FALSE) { | |
111 | return TRUE; | |
112 | } | |
113 | return !empty($field['option_group_id']); | |
114 | } | |
115 | ||
116 | /** | |
117 | * Get the data type from an array. Defaults to 'data_type' with fallback to | |
118 | * mapping for the integer value 'type' | |
119 | * | |
120 | * @param array $data | |
121 | * | |
122 | * @return string | |
123 | */ | |
124 | private static function getDataType(array $data) { | |
125 | if (isset($data['data_type'])) { | |
126 | return !empty($data['time_format']) ? 'Timestamp' : $data['data_type']; | |
127 | } | |
128 | ||
1b8e3bc8 | 129 | $dataTypeInt = $data['type'] ?? NULL; |
19b53e5b C |
130 | $dataTypeName = \CRM_Utils_Type::typeToString($dataTypeInt); |
131 | ||
132 | return $dataTypeName; | |
133 | } | |
134 | ||
2bf220fd CW |
135 | /** |
136 | * Callback function to build option lists for all DAO & custom fields. | |
137 | * | |
138 | * @param FieldSpec $spec | |
139 | * @param array $values | |
140 | * @param bool|array $returnFormat | |
141 | * @param bool $checkPermissions | |
142 | * @return array|false | |
143 | */ | |
144 | public static function getOptions($spec, $values, $returnFormat, $checkPermissions) { | |
145 | $fieldName = $spec->getName(); | |
146 | ||
147 | if ($spec instanceof CustomFieldSpec) { | |
148 | // buildOptions relies on the custom_* type of field names | |
149 | $fieldName = sprintf('custom_%d', $spec->getCustomFieldId()); | |
150 | } | |
151 | ||
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); | |
157 | ||
158 | if (!is_array($optionLabels) || !$optionLabels) { | |
159 | $options = FALSE; | |
160 | } | |
161 | else { | |
162 | $options = \CRM_Utils_Array::makeNonAssociative($optionLabels, 'id', 'label'); | |
163 | if (is_array($returnFormat)) { | |
164 | self::addOptionProps($options, $spec, $bao, $fieldName, $values, $returnFormat); | |
165 | } | |
166 | } | |
167 | return $options; | |
168 | } | |
169 | ||
170 | /** | |
171 | * Augment the 2 values returned by BAO::buildOptions (id, label) with extra properties (name, description, color, icon, etc). | |
172 | * | |
173 | * We start with BAO::buildOptions in order to respect hooks which may be adding/removing items, then we add the extra data. | |
174 | * | |
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 | |
181 | */ | |
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); | |
188 | } | |
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... | |
191 | if ($returnFormat) { | |
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'); | |
196 | } | |
197 | else { | |
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; | |
203 | } | |
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; | |
213 | } | |
214 | } | |
215 | } | |
216 | } | |
217 | else { | |
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']); | |
222 | } | |
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; | |
233 | } | |
234 | } | |
235 | } | |
236 | } | |
237 | } | |
238 | } | |
239 | if (isset($props)) { | |
240 | foreach ($options as &$option) { | |
241 | foreach ($props as $name => $prop) { | |
242 | $option[$name] = $prop[$option['id']] ?? NULL; | |
243 | } | |
244 | } | |
245 | } | |
246 | } | |
247 | ||
19b53e5b C |
248 | /** |
249 | * @param \Civi\Api4\Service\Spec\FieldSpec $fieldSpec | |
250 | * @param array $data | |
251 | * @param string $dataTypeName | |
252 | */ | |
253 | public static function setInputTypeAndAttrs(FieldSpec &$fieldSpec, $data, $dataTypeName) { | |
1b8e3bc8 CW |
254 | $inputType = $data['html']['type'] ?? $data['html_type'] ?? NULL; |
255 | $inputAttrs = $data['html'] ?? []; | |
19b53e5b C |
256 | unset($inputAttrs['type']); |
257 | ||
19b53e5b | 258 | $map = [ |
19b53e5b C |
259 | 'Select Date' => 'Date', |
260 | 'Link' => 'Url', | |
261 | ]; | |
1b8e3bc8 | 262 | $inputType = $map[$inputType] ?? $inputType; |
73cda6c5 CW |
263 | if ($dataTypeName === 'ContactReference') { |
264 | $inputType = 'EntityRef'; | |
265 | } | |
783a2874 | 266 | if (in_array($inputType, ['Select', 'EntityRef'], TRUE) && !empty($data['serialize'])) { |
08a06697 CW |
267 | $inputAttrs['multiple'] = TRUE; |
268 | } | |
19b53e5b C |
269 | if ($inputType == 'Date' && !empty($inputAttrs['formatType'])) { |
270 | self::setLegacyDateFormat($inputAttrs); | |
271 | } | |
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']; | |
278 | } | |
279 | if ($inputType == 'Text' && !empty($data['maxlength'])) { | |
280 | $inputAttrs['maxlength'] = (int) $data['maxlength']; | |
281 | } | |
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]; | |
286 | } | |
287 | } | |
288 | } | |
0dcd942c CW |
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; | |
295 | } | |
296 | } | |
19b53e5b C |
297 | $fieldSpec |
298 | ->setInputType($inputType) | |
299 | ->setInputAttrs($inputAttrs); | |
300 | } | |
301 | ||
302 | /** | |
303 | * @param array $inputAttrs | |
304 | */ | |
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']]); | |
310 | } | |
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']; | |
317 | } | |
318 | ||
319 | } |