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 | * $Id$ |
18 | * | |
19 | */ | |
20 | ||
21 | ||
19b53e5b C |
22 | namespace Civi\Api4\Utils; |
23 | ||
19b53e5b C |
24 | require_once 'api/v3/utils.php'; |
25 | ||
26 | class FormattingUtil { | |
27 | ||
961e974c CW |
28 | public static $pseudoConstantContexts = [ |
29 | 'name' => 'validate', | |
30 | 'abbr' => 'abbreviate', | |
31 | 'label' => 'get', | |
32 | ]; | |
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 | } | |
37d82abe | 49 | self::formatInputValue($value, $name, $field); |
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 | |
19b53e5b C |
83 | * @throws \API_Exception |
84 | */ | |
37d82abe | 85 | public static function formatInputValue(&$value, $fieldName, $fieldSpec) { |
961e974c CW |
86 | // Evaluate pseudoconstant suffix |
87 | $suffix = strpos($fieldName, ':'); | |
88 | if ($suffix) { | |
89 | $options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1)); | |
90 | $value = self::replacePseudoconstant($options, $value, TRUE); | |
16f5a13d | 91 | return; |
961e974c CW |
92 | } |
93 | elseif (is_array($value)) { | |
19b53e5b | 94 | foreach ($value as &$val) { |
37d82abe | 95 | self::formatInputValue($val, $fieldName, $fieldSpec); |
19b53e5b C |
96 | } |
97 | return; | |
98 | } | |
37d82abe | 99 | $fk = $fieldSpec['name'] == 'id' ? $fieldSpec['entity'] : $fieldSpec['fk_entity'] ?? NULL; |
19b53e5b C |
100 | |
101 | if ($fk === 'Domain' && $value === 'current_domain') { | |
102 | $value = \CRM_Core_Config::domainID(); | |
103 | } | |
104 | ||
105 | if ($fk === 'Contact' && !is_numeric($value)) { | |
106 | $value = \_civicrm_api3_resolve_contactID($value); | |
107 | if ('unknown-user' === $value) { | |
108 | throw new \API_Exception("\"{$fieldSpec['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $fieldSpec['name'], "type" => "integer"]); | |
109 | } | |
110 | } | |
111 | ||
2929a8fb | 112 | switch ($fieldSpec['data_type'] ?? NULL) { |
19b53e5b C |
113 | case 'Timestamp': |
114 | $value = date('Y-m-d H:i:s', strtotime($value)); | |
115 | break; | |
116 | ||
117 | case 'Date': | |
118 | $value = date('Ymd', strtotime($value)); | |
119 | break; | |
120 | } | |
c9b7a552 TO |
121 | |
122 | $hic = \CRM_Utils_API_HTMLInputCoder::singleton(); | |
123 | if (!$hic->isSkippedField($fieldSpec['name'])) { | |
124 | $value = $hic->encodeValue($value); | |
125 | } | |
19b53e5b C |
126 | } |
127 | ||
2929a8fb CW |
128 | /** |
129 | * Unserialize raw DAO values and convert to correct type | |
130 | * | |
131 | * @param array $results | |
132 | * @param array $fields | |
133 | * @param string $entity | |
961e974c CW |
134 | * @param string $action |
135 | * @throws \API_Exception | |
2929a8fb CW |
136 | * @throws \CRM_Core_Exception |
137 | */ | |
961e974c CW |
138 | public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') { |
139 | $fieldOptions = []; | |
2929a8fb | 140 | foreach ($results as &$result) { |
d818aa7b | 141 | $contactTypePaths = []; |
961e974c CW |
142 | foreach ($result as $fieldExpr => $value) { |
143 | $field = $fields[$fieldExpr] ?? NULL; | |
144 | $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL); | |
145 | if ($field) { | |
146 | // Evaluate pseudoconstant suffixes | |
147 | $suffix = strrpos($fieldExpr, ':'); | |
148 | if ($suffix) { | |
149 | $fieldName = empty($field['custom_field_id']) ? $field['name'] : 'custom_' . $field['custom_field_id']; | |
150 | $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field['entity'], $fieldName, substr($fieldExpr, $suffix + 1), $result, $action); | |
151 | $dataType = NULL; | |
152 | } | |
153 | if (!empty($field['serialize'])) { | |
154 | if (is_string($value)) { | |
155 | $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']); | |
2929a8fb CW |
156 | } |
157 | } | |
961e974c CW |
158 | if (isset($fieldOptions[$fieldExpr])) { |
159 | $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value); | |
160 | } | |
d818aa7b CW |
161 | // Keep track of contact types for self::contactFieldsToRemove |
162 | if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') { | |
163 | $prefix = strrpos($fieldExpr, '.'); | |
164 | $contactTypePaths[$prefix ? substr($fieldExpr, 0, $prefix + 1) : ''] = $value; | |
165 | } | |
2929a8fb | 166 | } |
961e974c CW |
167 | $result[$fieldExpr] = self::convertDataType($value, $dataType); |
168 | } | |
d818aa7b CW |
169 | // Remove inapplicable contact fields |
170 | foreach ($contactTypePaths as $prefix => $contactType) { | |
171 | \CRM_Utils_Array::remove($result, self::contactFieldsToRemove($contactType, $prefix)); | |
172 | } | |
961e974c CW |
173 | } |
174 | } | |
175 | ||
176 | /** | |
177 | * Retrieves pseudoconstant option list for a field. | |
178 | * | |
179 | * @param string $entity | |
180 | * Name of api entity | |
181 | * @param string $fieldName | |
bb6bfd68 CW |
182 | * @param string $valueType |
183 | * name|label|abbr from self::$pseudoConstantContexts | |
961e974c CW |
184 | * @param array $params |
185 | * Other values for this object | |
186 | * @param string $action | |
187 | * @return array | |
188 | * @throws \API_Exception | |
189 | */ | |
bb6bfd68 CW |
190 | public static function getPseudoconstantList($entity, $fieldName, $valueType, $params = [], $action = 'get') { |
191 | $context = self::$pseudoConstantContexts[$valueType] ?? NULL; | |
961e974c CW |
192 | if (!$context) { |
193 | throw new \API_Exception('Illegal expression'); | |
194 | } | |
195 | $baoName = CoreUtil::getBAOFromApiName($entity); | |
196 | // Use BAO::buildOptions if possible | |
197 | if ($baoName) { | |
198 | $options = $baoName::buildOptions($fieldName, $context, $params); | |
199 | } | |
bb6bfd68 CW |
200 | // Fallback for option lists that exist in the api but not the BAO - note: $valueType gets ignored here |
201 | if (!isset($options) || $options === FALSE) { | |
961e974c CW |
202 | $options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => TRUE, 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL; |
203 | } | |
204 | if (is_array($options)) { | |
205 | return $options; | |
206 | } | |
207 | throw new \API_Exception("No option list found for '$fieldName'"); | |
208 | } | |
209 | ||
210 | /** | |
211 | * Replaces value (or an array of values) with options from a pseudoconstant list. | |
212 | * | |
213 | * The direction of lookup defaults to transforming ids to option values for api output; | |
214 | * for api input, set $reverse = TRUE to transform option values to ids. | |
215 | * | |
216 | * @param array $options | |
217 | * @param string|string[] $value | |
218 | * @param bool $reverse | |
219 | * Is this a reverse lookup (for transforming input instead of output) | |
220 | * @return array|mixed|null | |
221 | */ | |
222 | public static function replacePseudoconstant($options, $value, $reverse = FALSE) { | |
223 | $matches = []; | |
224 | foreach ((array) $value as $val) { | |
225 | if (!$reverse && isset($options[$val])) { | |
226 | $matches[] = $options[$val]; | |
227 | } | |
228 | elseif ($reverse && array_search($val, $options) !== FALSE) { | |
229 | $matches[] = array_search($val, $options); | |
2929a8fb CW |
230 | } |
231 | } | |
961e974c | 232 | return is_array($value) ? $matches : $matches[0] ?? NULL; |
2929a8fb CW |
233 | } |
234 | ||
235 | /** | |
236 | * @param mixed $value | |
237 | * @param string $dataType | |
238 | * @return mixed | |
239 | */ | |
240 | public static function convertDataType($value, $dataType) { | |
961e974c CW |
241 | if (isset($value) && $dataType) { |
242 | if (is_array($value)) { | |
243 | foreach ($value as $key => $val) { | |
244 | $value[$key] = self::convertDataType($val, $dataType); | |
245 | } | |
246 | return $value; | |
247 | } | |
248 | ||
2929a8fb CW |
249 | switch ($dataType) { |
250 | case 'Boolean': | |
251 | return (bool) $value; | |
252 | ||
253 | case 'Integer': | |
254 | return (int) $value; | |
255 | ||
256 | case 'Money': | |
257 | case 'Float': | |
258 | return (float) $value; | |
259 | } | |
260 | } | |
261 | return $value; | |
262 | } | |
263 | ||
264 | /** | |
d818aa7b CW |
265 | * Lists all field names (including suffixed variants) that should be removed for a given contact type. |
266 | * | |
2929a8fb | 267 | * @param string $contactType |
d818aa7b CW |
268 | * Individual|Organization|Household |
269 | * @param string $prefix | |
270 | * Path at which these fields are found, e.g. "address.contact." | |
2929a8fb CW |
271 | * @return array |
272 | */ | |
d818aa7b | 273 | public static function contactFieldsToRemove($contactType, $prefix) { |
2929a8fb CW |
274 | if (!isset(\Civi::$statics[__CLASS__][__FUNCTION__][$contactType])) { |
275 | \Civi::$statics[__CLASS__][__FUNCTION__][$contactType] = []; | |
276 | foreach (\CRM_Contact_DAO_Contact::fields() as $field) { | |
277 | if (!empty($field['contactType']) && $field['contactType'] != $contactType) { | |
278 | \Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name']; | |
d818aa7b CW |
279 | // Include suffixed variants like prefix_id:label |
280 | if (!empty($field['pseudoconstant'])) { | |
281 | foreach (array_keys(self::$pseudoConstantContexts) as $suffix) { | |
282 | \Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name'] . ':' . $suffix; | |
283 | } | |
284 | } | |
2929a8fb CW |
285 | } |
286 | } | |
287 | } | |
d818aa7b CW |
288 | // Add prefix paths |
289 | return array_map(function($name) use ($prefix) { | |
290 | return $prefix . $name; | |
291 | }, \Civi::$statics[__CLASS__][__FUNCTION__][$contactType]); | |
2929a8fb CW |
292 | } |
293 | ||
19b53e5b | 294 | } |