Merge pull request #17368 from colemanw/api4suffix
[civicrm-core.git] / Civi / Api4 / Utils / FormattingUtil.php
CommitLineData
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
22namespace Civi\Api4\Utils;
23
19b53e5b
C
24require_once 'api/v3/utils.php';
25
26class 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}