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 | ||
13 | /** | |
14 | * | |
15 | * @package CRM | |
ca5cec67 | 16 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
380f3545 TO |
17 | */ |
18 | ||
19 | ||
19b53e5b C |
20 | namespace Civi\Api4\Utils; |
21 | ||
19b53e5b C |
22 | require_once 'api/v3/utils.php'; |
23 | ||
24 | class FormattingUtil { | |
25 | ||
961e974c CW |
26 | public static $pseudoConstantContexts = [ |
27 | 'name' => 'validate', | |
28 | 'abbr' => 'abbreviate', | |
29 | 'label' => 'get', | |
30 | ]; | |
31 | ||
3ffbd21c CW |
32 | public static $pseudoConstantSuffixes = ['name', 'abbr', 'label', 'color', 'description', 'icon']; |
33 | ||
19b53e5b C |
34 | /** |
35 | * Massage values into the format the BAO expects for a write operation | |
36 | * | |
37d82abe CW |
37 | * @param array $params |
38 | * @param array $fields | |
19b53e5b C |
39 | * @throws \API_Exception |
40 | */ | |
37d82abe | 41 | public static function formatWriteParams(&$params, $fields) { |
19b53e5b C |
42 | foreach ($fields as $name => $field) { |
43 | if (!empty($params[$name])) { | |
44 | $value =& $params[$name]; | |
45 | // Hack for null values -- see comment below | |
46 | if ($value === 'null') { | |
47 | $value = 'Null'; | |
48 | } | |
3ffbd21c | 49 | self::formatInputValue($value, $name, $field, 'create'); |
19b53e5b C |
50 | // Ensure we have an array for serialized fields |
51 | if (!empty($field['serialize'] && !is_array($value))) { | |
52 | $value = (array) $value; | |
53 | } | |
54 | } | |
55 | /* | |
56 | * Because of the wacky way that database values are saved we need to format | |
57 | * some of the values here. In this strange world the string 'null' is used to | |
58 | * unset values. Hence if we encounter true null we change it to string 'null'. | |
59 | * | |
60 | * If we encounter the string 'null' then we assume the user actually wants to | |
61 | * set the value to string null. However since the string null is reserved for | |
62 | * unsetting values we must change it. Another quirk of the DB_DataObject is | |
63 | * that it allows 'Null' to be set, but any other variation of string 'null' | |
64 | * will be converted to true null, e.g. 'nuLL', 'NUlL' etc. so we change it to | |
65 | * 'Null'. | |
66 | */ | |
67 | elseif (array_key_exists($name, $params) && $params[$name] === NULL) { | |
68 | $params[$name] = 'null'; | |
69 | } | |
70 | } | |
c9b7a552 TO |
71 | |
72 | \CRM_Utils_API_HTMLInputCoder::singleton()->encodeRow($params); | |
19b53e5b C |
73 | } |
74 | ||
75 | /** | |
76 | * Transform raw api input to appropriate format for use in a SQL query. | |
77 | * | |
78 | * This is used by read AND write actions (Get, Create, Update, Replace) | |
79 | * | |
80 | * @param $value | |
961e974c CW |
81 | * @param string $fieldName |
82 | * @param array $fieldSpec | |
3ffbd21c | 83 | * @param string $action |
19b53e5b | 84 | * @throws \API_Exception |
3ffbd21c | 85 | * @throws \CRM_Core_Exception |
19b53e5b | 86 | */ |
3ffbd21c | 87 | public static function formatInputValue(&$value, $fieldName, $fieldSpec, $action = 'get') { |
961e974c CW |
88 | // Evaluate pseudoconstant suffix |
89 | $suffix = strpos($fieldName, ':'); | |
90 | if ($suffix) { | |
3ffbd21c | 91 | $options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1), $action); |
961e974c | 92 | $value = self::replacePseudoconstant($options, $value, TRUE); |
16f5a13d | 93 | return; |
961e974c CW |
94 | } |
95 | elseif (is_array($value)) { | |
19b53e5b | 96 | foreach ($value as &$val) { |
3ffbd21c | 97 | self::formatInputValue($val, $fieldName, $fieldSpec, $action); |
19b53e5b C |
98 | } |
99 | return; | |
100 | } | |
37d82abe | 101 | $fk = $fieldSpec['name'] == 'id' ? $fieldSpec['entity'] : $fieldSpec['fk_entity'] ?? NULL; |
19b53e5b C |
102 | |
103 | if ($fk === 'Domain' && $value === 'current_domain') { | |
104 | $value = \CRM_Core_Config::domainID(); | |
105 | } | |
106 | ||
107 | if ($fk === 'Contact' && !is_numeric($value)) { | |
108 | $value = \_civicrm_api3_resolve_contactID($value); | |
109 | if ('unknown-user' === $value) { | |
110 | throw new \API_Exception("\"{$fieldSpec['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $fieldSpec['name'], "type" => "integer"]); | |
111 | } | |
112 | } | |
113 | ||
2929a8fb | 114 | switch ($fieldSpec['data_type'] ?? NULL) { |
19b53e5b C |
115 | case 'Timestamp': |
116 | $value = date('Y-m-d H:i:s', strtotime($value)); | |
117 | break; | |
118 | ||
119 | case 'Date': | |
120 | $value = date('Ymd', strtotime($value)); | |
121 | break; | |
122 | } | |
c9b7a552 TO |
123 | |
124 | $hic = \CRM_Utils_API_HTMLInputCoder::singleton(); | |
125 | if (!$hic->isSkippedField($fieldSpec['name'])) { | |
126 | $value = $hic->encodeValue($value); | |
127 | } | |
19b53e5b C |
128 | } |
129 | ||
2929a8fb CW |
130 | /** |
131 | * Unserialize raw DAO values and convert to correct type | |
132 | * | |
133 | * @param array $results | |
134 | * @param array $fields | |
135 | * @param string $entity | |
961e974c CW |
136 | * @param string $action |
137 | * @throws \API_Exception | |
2929a8fb CW |
138 | * @throws \CRM_Core_Exception |
139 | */ | |
961e974c CW |
140 | public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') { |
141 | $fieldOptions = []; | |
2929a8fb | 142 | foreach ($results as &$result) { |
d818aa7b | 143 | $contactTypePaths = []; |
961e974c CW |
144 | foreach ($result as $fieldExpr => $value) { |
145 | $field = $fields[$fieldExpr] ?? NULL; | |
146 | $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL); | |
147 | if ($field) { | |
148 | // Evaluate pseudoconstant suffixes | |
149 | $suffix = strrpos($fieldExpr, ':'); | |
150 | if ($suffix) { | |
151 | $fieldName = empty($field['custom_field_id']) ? $field['name'] : 'custom_' . $field['custom_field_id']; | |
152 | $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field['entity'], $fieldName, substr($fieldExpr, $suffix + 1), $result, $action); | |
153 | $dataType = NULL; | |
154 | } | |
155 | if (!empty($field['serialize'])) { | |
156 | if (is_string($value)) { | |
157 | $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']); | |
2929a8fb CW |
158 | } |
159 | } | |
961e974c CW |
160 | if (isset($fieldOptions[$fieldExpr])) { |
161 | $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value); | |
162 | } | |
d818aa7b CW |
163 | // Keep track of contact types for self::contactFieldsToRemove |
164 | if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') { | |
165 | $prefix = strrpos($fieldExpr, '.'); | |
166 | $contactTypePaths[$prefix ? substr($fieldExpr, 0, $prefix + 1) : ''] = $value; | |
167 | } | |
2929a8fb | 168 | } |
961e974c CW |
169 | $result[$fieldExpr] = self::convertDataType($value, $dataType); |
170 | } | |
d818aa7b CW |
171 | // Remove inapplicable contact fields |
172 | foreach ($contactTypePaths as $prefix => $contactType) { | |
173 | \CRM_Utils_Array::remove($result, self::contactFieldsToRemove($contactType, $prefix)); | |
174 | } | |
961e974c CW |
175 | } |
176 | } | |
177 | ||
178 | /** | |
179 | * Retrieves pseudoconstant option list for a field. | |
180 | * | |
181 | * @param string $entity | |
182 | * Name of api entity | |
183 | * @param string $fieldName | |
bb6bfd68 CW |
184 | * @param string $valueType |
185 | * name|label|abbr from self::$pseudoConstantContexts | |
961e974c CW |
186 | * @param array $params |
187 | * Other values for this object | |
188 | * @param string $action | |
189 | * @return array | |
190 | * @throws \API_Exception | |
191 | */ | |
bb6bfd68 CW |
192 | public static function getPseudoconstantList($entity, $fieldName, $valueType, $params = [], $action = 'get') { |
193 | $context = self::$pseudoConstantContexts[$valueType] ?? NULL; | |
3ffbd21c CW |
194 | // For create actions, only unique identifiers can be used. |
195 | // For get actions any valid suffix is ok. | |
196 | if (($action === 'create' && !$context) || !in_array($valueType, self::$pseudoConstantSuffixes, TRUE)) { | |
961e974c CW |
197 | throw new \API_Exception('Illegal expression'); |
198 | } | |
3ffbd21c | 199 | $baoName = $context ? CoreUtil::getBAOFromApiName($entity) : NULL; |
961e974c CW |
200 | // Use BAO::buildOptions if possible |
201 | if ($baoName) { | |
202 | $options = $baoName::buildOptions($fieldName, $context, $params); | |
203 | } | |
3ffbd21c | 204 | // Fallback for option lists that exist in the api but not the BAO |
bb6bfd68 | 205 | if (!isset($options) || $options === FALSE) { |
3ffbd21c CW |
206 | $options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => ['id', $valueType], 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL; |
207 | $options = $options ? array_column($options, $valueType, 'id') : $options; | |
961e974c CW |
208 | } |
209 | if (is_array($options)) { | |
210 | return $options; | |
211 | } | |
212 | throw new \API_Exception("No option list found for '$fieldName'"); | |
213 | } | |
214 | ||
215 | /** | |
216 | * Replaces value (or an array of values) with options from a pseudoconstant list. | |
217 | * | |
218 | * The direction of lookup defaults to transforming ids to option values for api output; | |
219 | * for api input, set $reverse = TRUE to transform option values to ids. | |
220 | * | |
221 | * @param array $options | |
222 | * @param string|string[] $value | |
223 | * @param bool $reverse | |
224 | * Is this a reverse lookup (for transforming input instead of output) | |
225 | * @return array|mixed|null | |
226 | */ | |
227 | public static function replacePseudoconstant($options, $value, $reverse = FALSE) { | |
228 | $matches = []; | |
229 | foreach ((array) $value as $val) { | |
230 | if (!$reverse && isset($options[$val])) { | |
231 | $matches[] = $options[$val]; | |
232 | } | |
233 | elseif ($reverse && array_search($val, $options) !== FALSE) { | |
234 | $matches[] = array_search($val, $options); | |
2929a8fb CW |
235 | } |
236 | } | |
961e974c | 237 | return is_array($value) ? $matches : $matches[0] ?? NULL; |
2929a8fb CW |
238 | } |
239 | ||
240 | /** | |
241 | * @param mixed $value | |
242 | * @param string $dataType | |
243 | * @return mixed | |
244 | */ | |
245 | public static function convertDataType($value, $dataType) { | |
961e974c CW |
246 | if (isset($value) && $dataType) { |
247 | if (is_array($value)) { | |
248 | foreach ($value as $key => $val) { | |
249 | $value[$key] = self::convertDataType($val, $dataType); | |
250 | } | |
251 | return $value; | |
252 | } | |
253 | ||
2929a8fb CW |
254 | switch ($dataType) { |
255 | case 'Boolean': | |
256 | return (bool) $value; | |
257 | ||
258 | case 'Integer': | |
259 | return (int) $value; | |
260 | ||
261 | case 'Money': | |
262 | case 'Float': | |
263 | return (float) $value; | |
264 | } | |
265 | } | |
266 | return $value; | |
267 | } | |
268 | ||
269 | /** | |
d818aa7b CW |
270 | * Lists all field names (including suffixed variants) that should be removed for a given contact type. |
271 | * | |
2929a8fb | 272 | * @param string $contactType |
d818aa7b CW |
273 | * Individual|Organization|Household |
274 | * @param string $prefix | |
275 | * Path at which these fields are found, e.g. "address.contact." | |
2929a8fb CW |
276 | * @return array |
277 | */ | |
d818aa7b | 278 | public static function contactFieldsToRemove($contactType, $prefix) { |
2929a8fb CW |
279 | if (!isset(\Civi::$statics[__CLASS__][__FUNCTION__][$contactType])) { |
280 | \Civi::$statics[__CLASS__][__FUNCTION__][$contactType] = []; | |
281 | foreach (\CRM_Contact_DAO_Contact::fields() as $field) { | |
282 | if (!empty($field['contactType']) && $field['contactType'] != $contactType) { | |
283 | \Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name']; | |
d818aa7b CW |
284 | // Include suffixed variants like prefix_id:label |
285 | if (!empty($field['pseudoconstant'])) { | |
3ffbd21c | 286 | foreach (self::$pseudoConstantSuffixes as $suffix) { |
d818aa7b CW |
287 | \Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name'] . ':' . $suffix; |
288 | } | |
289 | } | |
2929a8fb CW |
290 | } |
291 | } | |
292 | } | |
d818aa7b CW |
293 | // Add prefix paths |
294 | return array_map(function($name) use ($prefix) { | |
295 | return $prefix . $name; | |
296 | }, \Civi::$statics[__CLASS__][__FUNCTION__][$contactType]); | |
2929a8fb CW |
297 | } |
298 | ||
19b53e5b | 299 | } |