APIv4 - Support pseudoconstant lookups
[civicrm-core.git] / Civi / Api4 / Utils / FormattingUtil.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
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 +--------------------------------------------------------------------+
11 */
12
13 /**
14 *
15 * @package CRM
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 * $Id$
18 *
19 */
20
21
22 namespace Civi\Api4\Utils;
23
24 require_once 'api/v3/utils.php';
25
26 class FormattingUtil {
27
28 public static $pseudoConstantContexts = [
29 'name' => 'validate',
30 'abbr' => 'abbreviate',
31 'label' => 'get',
32 ];
33
34 /**
35 * Massage values into the format the BAO expects for a write operation
36 *
37 * @param $params
38 * @param $entity
39 * @param $fields
40 * @throws \API_Exception
41 */
42 public static function formatWriteParams(&$params, $entity, $fields) {
43 foreach ($fields as $name => $field) {
44 if (!empty($params[$name])) {
45 $value =& $params[$name];
46 // Hack for null values -- see comment below
47 if ($value === 'null') {
48 $value = 'Null';
49 }
50 self::formatInputValue($value, $name, $field, $entity);
51 // Ensure we have an array for serialized fields
52 if (!empty($field['serialize'] && !is_array($value))) {
53 $value = (array) $value;
54 }
55 }
56 /*
57 * Because of the wacky way that database values are saved we need to format
58 * some of the values here. In this strange world the string 'null' is used to
59 * unset values. Hence if we encounter true null we change it to string 'null'.
60 *
61 * If we encounter the string 'null' then we assume the user actually wants to
62 * set the value to string null. However since the string null is reserved for
63 * unsetting values we must change it. Another quirk of the DB_DataObject is
64 * that it allows 'Null' to be set, but any other variation of string 'null'
65 * will be converted to true null, e.g. 'nuLL', 'NUlL' etc. so we change it to
66 * 'Null'.
67 */
68 elseif (array_key_exists($name, $params) && $params[$name] === NULL) {
69 $params[$name] = 'null';
70 }
71 }
72
73 \CRM_Utils_API_HTMLInputCoder::singleton()->encodeRow($params);
74 }
75
76 /**
77 * Transform raw api input to appropriate format for use in a SQL query.
78 *
79 * This is used by read AND write actions (Get, Create, Update, Replace)
80 *
81 * @param $value
82 * @param string $fieldName
83 * @param array $fieldSpec
84 * @param string $entity
85 * Ex: 'Contact', 'Domain'
86 * @throws \API_Exception
87 */
88 public static function formatInputValue(&$value, $fieldName, $fieldSpec, $entity) {
89 // Evaluate pseudoconstant suffix
90 $suffix = strpos($fieldName, ':');
91 if ($suffix) {
92 $options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1));
93 $value = self::replacePseudoconstant($options, $value, TRUE);
94 }
95 elseif (is_array($value)) {
96 foreach ($value as &$val) {
97 self::formatInputValue($val, $fieldName, $fieldSpec, $entity);
98 }
99 return;
100 }
101 $fk = $fieldSpec['name'] == 'id' ? $entity : $fieldSpec['fk_entity'] ?? NULL;
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
114 switch ($fieldSpec['data_type'] ?? NULL) {
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 }
123
124 $hic = \CRM_Utils_API_HTMLInputCoder::singleton();
125 if (!$hic->isSkippedField($fieldSpec['name'])) {
126 $value = $hic->encodeValue($value);
127 }
128 }
129
130 /**
131 * Unserialize raw DAO values and convert to correct type
132 *
133 * @param array $results
134 * @param array $fields
135 * @param string $entity
136 * @param string $action
137 * @throws \API_Exception
138 * @throws \CRM_Core_Exception
139 */
140 public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') {
141 $fieldOptions = [];
142 foreach ($results as &$result) {
143 // Remove inapplicable contact fields
144 if ($entity === 'Contact' && !empty($result['contact_type'])) {
145 \CRM_Utils_Array::remove($result, self::contactFieldsToRemove($result['contact_type']));
146 }
147 foreach ($result as $fieldExpr => $value) {
148 $field = $fields[$fieldExpr] ?? NULL;
149 $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL);
150 if ($field) {
151 // Evaluate pseudoconstant suffixes
152 $suffix = strrpos($fieldExpr, ':');
153 if ($suffix) {
154 $fieldName = empty($field['custom_field_id']) ? $field['name'] : 'custom_' . $field['custom_field_id'];
155 $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field['entity'], $fieldName, substr($fieldExpr, $suffix + 1), $result, $action);
156 $dataType = NULL;
157 }
158 if (!empty($field['serialize'])) {
159 if (is_string($value)) {
160 $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']);
161 }
162 }
163 if (isset($fieldOptions[$fieldExpr])) {
164 $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value);
165 }
166 }
167 $result[$fieldExpr] = self::convertDataType($value, $dataType);
168 }
169 }
170 }
171
172 /**
173 * Retrieves pseudoconstant option list for a field.
174 *
175 * @param string $entity
176 * Name of api entity
177 * @param string $fieldName
178 * @param string $optionValue
179 * @param array $params
180 * Other values for this object
181 * @param string $action
182 * @return array
183 * @throws \API_Exception
184 */
185 public static function getPseudoconstantList($entity, $fieldName, $optionValue, $params = [], $action = 'get') {
186 $context = self::$pseudoConstantContexts[$optionValue] ?? NULL;
187 if (!$context) {
188 throw new \API_Exception('Illegal expression');
189 }
190 $baoName = CoreUtil::getBAOFromApiName($entity);
191 // Use BAO::buildOptions if possible
192 if ($baoName) {
193 $options = $baoName::buildOptions($fieldName, $context, $params);
194 }
195 // Fallback for non-bao based entities
196 if (!isset($options)) {
197 $options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => TRUE, 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL;
198 }
199 if (is_array($options)) {
200 return $options;
201 }
202 throw new \API_Exception("No option list found for '$fieldName'");
203 }
204
205 /**
206 * Replaces value (or an array of values) with options from a pseudoconstant list.
207 *
208 * The direction of lookup defaults to transforming ids to option values for api output;
209 * for api input, set $reverse = TRUE to transform option values to ids.
210 *
211 * @param array $options
212 * @param string|string[] $value
213 * @param bool $reverse
214 * Is this a reverse lookup (for transforming input instead of output)
215 * @return array|mixed|null
216 */
217 public static function replacePseudoconstant($options, $value, $reverse = FALSE) {
218 $matches = [];
219 foreach ((array) $value as $val) {
220 if (!$reverse && isset($options[$val])) {
221 $matches[] = $options[$val];
222 }
223 elseif ($reverse && array_search($val, $options) !== FALSE) {
224 $matches[] = array_search($val, $options);
225 }
226 }
227 return is_array($value) ? $matches : $matches[0] ?? NULL;
228 }
229
230 /**
231 * @param mixed $value
232 * @param string $dataType
233 * @return mixed
234 */
235 public static function convertDataType($value, $dataType) {
236 if (isset($value) && $dataType) {
237 if (is_array($value)) {
238 foreach ($value as $key => $val) {
239 $value[$key] = self::convertDataType($val, $dataType);
240 }
241 return $value;
242 }
243
244 switch ($dataType) {
245 case 'Boolean':
246 return (bool) $value;
247
248 case 'Integer':
249 return (int) $value;
250
251 case 'Money':
252 case 'Float':
253 return (float) $value;
254 }
255 }
256 return $value;
257 }
258
259 /**
260 * @param string $contactType
261 * @return array
262 */
263 public static function contactFieldsToRemove($contactType) {
264 if (!isset(\Civi::$statics[__CLASS__][__FUNCTION__][$contactType])) {
265 \Civi::$statics[__CLASS__][__FUNCTION__][$contactType] = [];
266 foreach (\CRM_Contact_DAO_Contact::fields() as $field) {
267 if (!empty($field['contactType']) && $field['contactType'] != $contactType) {
268 \Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name'];
269 }
270 }
271 }
272 return \Civi::$statics[__CLASS__][__FUNCTION__][$contactType];
273 }
274
275 }