Fix for new prefetch key
[civicrm-core.git] / Civi / Api4 / Service / Spec / SpecFormatter.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\Service\Spec;
14
15 use Civi\Api4\Utils\CoreUtil;
16 use CRM_Core_DAO_AllCoreTables as AllCoreTables;
17
18 class SpecFormatter {
19
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) {
32 $field->setName($data['custom_group_id.name'] . '.' . $data['name']);
33 }
34 else {
35 $field->setTableName($data['custom_group_id.table_name']);
36 }
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']);
50 }
51 }
52 $field->setReadonly($data['is_view']);
53 }
54 else {
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']);
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);
76 }
77 $field->setReadonly(!empty($data['readonly']));
78 }
79 $field->setSerialize($data['serialize'] ?? NULL);
80 $field->setDefaultValue($data['default'] ?? NULL);
81 $field->setDescription($data['description'] ?? NULL);
82 self::setInputTypeAndAttrs($field, $data, $dataTypeName);
83
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));
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
129 $dataTypeInt = $data['type'] ?? NULL;
130 $dataTypeName = \CRM_Utils_Type::typeToString($dataTypeInt);
131
132 return $dataTypeName;
133 }
134
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
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) {
254 $inputType = $data['html']['type'] ?? $data['html_type'] ?? NULL;
255 $inputAttrs = $data['html'] ?? [];
256 unset($inputAttrs['type']);
257
258 $map = [
259 'Select Date' => 'Date',
260 'Link' => 'Url',
261 ];
262 $inputType = $map[$inputType] ?? $inputType;
263 if ($dataTypeName === 'ContactReference') {
264 $inputType = 'EntityRef';
265 }
266 if (in_array($inputType, ['Select', 'EntityRef'], TRUE) && !empty($data['serialize'])) {
267 $inputAttrs['multiple'] = TRUE;
268 }
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 }
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 }
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 }