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