4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
22 namespace Civi\Api4\Utils
;
24 require_once 'api/v3/utils.php';
26 class FormattingUtil
{
28 public static $pseudoConstantContexts = [
30 'abbr' => 'abbreviate',
35 * Massage values into the format the BAO expects for a write operation
37 * @param array $params
38 * @param array $fields
39 * @throws \API_Exception
41 public static function formatWriteParams(&$params, $fields) {
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') {
49 self
::formatInputValue($value, $name, $field);
50 // Ensure we have an array for serialized fields
51 if (!empty($field['serialize'] && !is_array($value))) {
52 $value = (array) $value;
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'.
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
67 elseif (array_key_exists($name, $params) && $params[$name] === NULL) {
68 $params[$name] = 'null';
72 \CRM_Utils_API_HTMLInputCoder
::singleton()->encodeRow($params);
76 * Transform raw api input to appropriate format for use in a SQL query.
78 * This is used by read AND write actions (Get, Create, Update, Replace)
81 * @param string $fieldName
82 * @param array $fieldSpec
83 * @throws \API_Exception
85 public static function formatInputValue(&$value, $fieldName, $fieldSpec) {
86 // Evaluate pseudoconstant suffix
87 $suffix = strpos($fieldName, ':');
89 $options = self
::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix +
1));
90 $value = self
::replacePseudoconstant($options, $value, TRUE);
92 elseif (is_array($value)) {
93 foreach ($value as &$val) {
94 self
::formatInputValue($val, $fieldName, $fieldSpec);
98 $fk = $fieldSpec['name'] == 'id' ?
$fieldSpec['entity'] : $fieldSpec['fk_entity'] ??
NULL;
100 if ($fk === 'Domain' && $value === 'current_domain') {
101 $value = \CRM_Core_Config
::domainID();
104 if ($fk === 'Contact' && !is_numeric($value)) {
105 $value = \
_civicrm_api3_resolve_contactID($value);
106 if ('unknown-user' === $value) {
107 throw new \
API_Exception("\"{$fieldSpec['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $fieldSpec['name'], "type" => "integer"]);
111 switch ($fieldSpec['data_type'] ??
NULL) {
113 $value = date('Y-m-d H:i:s', strtotime($value));
117 $value = date('Ymd', strtotime($value));
121 $hic = \CRM_Utils_API_HTMLInputCoder
::singleton();
122 if (!$hic->isSkippedField($fieldSpec['name'])) {
123 $value = $hic->encodeValue($value);
128 * Unserialize raw DAO values and convert to correct type
130 * @param array $results
131 * @param array $fields
132 * @param string $entity
133 * @param string $action
134 * @throws \API_Exception
135 * @throws \CRM_Core_Exception
137 public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') {
139 foreach ($results as &$result) {
140 $contactTypePaths = [];
141 foreach ($result as $fieldExpr => $value) {
142 $field = $fields[$fieldExpr] ??
NULL;
143 $dataType = $field['data_type'] ??
($fieldExpr == 'id' ?
'Integer' : NULL);
145 // Evaluate pseudoconstant suffixes
146 $suffix = strrpos($fieldExpr, ':');
148 $fieldName = empty($field['custom_field_id']) ?
$field['name'] : 'custom_' . $field['custom_field_id'];
149 $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self
::getPseudoconstantList($field['entity'], $fieldName, substr($fieldExpr, $suffix +
1), $result, $action);
152 if (!empty($field['serialize'])) {
153 if (is_string($value)) {
154 $value = \CRM_Core_DAO
::unSerializeField($value, $field['serialize']);
157 if (isset($fieldOptions[$fieldExpr])) {
158 $value = self
::replacePseudoconstant($fieldOptions[$fieldExpr], $value);
160 // Keep track of contact types for self::contactFieldsToRemove
161 if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') {
162 $prefix = strrpos($fieldExpr, '.');
163 $contactTypePaths[$prefix ?
substr($fieldExpr, 0, $prefix +
1) : ''] = $value;
166 $result[$fieldExpr] = self
::convertDataType($value, $dataType);
168 // Remove inapplicable contact fields
169 foreach ($contactTypePaths as $prefix => $contactType) {
170 \CRM_Utils_Array
::remove($result, self
::contactFieldsToRemove($contactType, $prefix));
176 * Retrieves pseudoconstant option list for a field.
178 * @param string $entity
180 * @param string $fieldName
181 * @param string $valueType
182 * name|label|abbr from self::$pseudoConstantContexts
183 * @param array $params
184 * Other values for this object
185 * @param string $action
187 * @throws \API_Exception
189 public static function getPseudoconstantList($entity, $fieldName, $valueType, $params = [], $action = 'get') {
190 $context = self
::$pseudoConstantContexts[$valueType] ??
NULL;
192 throw new \
API_Exception('Illegal expression');
194 $baoName = CoreUtil
::getBAOFromApiName($entity);
195 // Use BAO::buildOptions if possible
197 $options = $baoName::buildOptions($fieldName, $context, $params);
199 // Fallback for option lists that exist in the api but not the BAO - note: $valueType gets ignored here
200 if (!isset($options) ||
$options === FALSE) {
201 $options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => TRUE, 'where' => [['name', '=', $fieldName]]])[0]['options'] ??
NULL;
203 if (is_array($options)) {
206 throw new \
API_Exception("No option list found for '$fieldName'");
210 * Replaces value (or an array of values) with options from a pseudoconstant list.
212 * The direction of lookup defaults to transforming ids to option values for api output;
213 * for api input, set $reverse = TRUE to transform option values to ids.
215 * @param array $options
216 * @param string|string[] $value
217 * @param bool $reverse
218 * Is this a reverse lookup (for transforming input instead of output)
219 * @return array|mixed|null
221 public static function replacePseudoconstant($options, $value, $reverse = FALSE) {
223 foreach ((array) $value as $val) {
224 if (!$reverse && isset($options[$val])) {
225 $matches[] = $options[$val];
227 elseif ($reverse && array_search($val, $options) !== FALSE) {
228 $matches[] = array_search($val, $options);
231 return is_array($value) ?
$matches : $matches[0] ??
NULL;
235 * @param mixed $value
236 * @param string $dataType
239 public static function convertDataType($value, $dataType) {
240 if (isset($value) && $dataType) {
241 if (is_array($value)) {
242 foreach ($value as $key => $val) {
243 $value[$key] = self
::convertDataType($val, $dataType);
250 return (bool) $value;
257 return (float) $value;
264 * Lists all field names (including suffixed variants) that should be removed for a given contact type.
266 * @param string $contactType
267 * Individual|Organization|Household
268 * @param string $prefix
269 * Path at which these fields are found, e.g. "address.contact."
272 public static function contactFieldsToRemove($contactType, $prefix) {
273 if (!isset(\Civi
::$statics[__CLASS__
][__FUNCTION__
][$contactType])) {
274 \Civi
::$statics[__CLASS__
][__FUNCTION__
][$contactType] = [];
275 foreach (\CRM_Contact_DAO_Contact
::fields() as $field) {
276 if (!empty($field['contactType']) && $field['contactType'] != $contactType) {
277 \Civi
::$statics[__CLASS__
][__FUNCTION__
][$contactType][] = $field['name'];
278 // Include suffixed variants like prefix_id:label
279 if (!empty($field['pseudoconstant'])) {
280 foreach (array_keys(self
::$pseudoConstantContexts) as $suffix) {
281 \Civi
::$statics[__CLASS__
][__FUNCTION__
][$contactType][] = $field['name'] . ':' . $suffix;
288 return array_map(function($name) use ($prefix) {
289 return $prefix . $name;
290 }, \Civi
::$statics[__CLASS__
][__FUNCTION__
][$contactType]);