SearchKit - Improve pseudoconstant support & tests for SearchDisplay::getDefault
[civicrm-core.git] / Civi / Api4 / Generic / Traits / DAOActionTrait.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 namespace Civi\Api4\Generic\Traits;
14
15 use Civi\Api4\CustomField;
16 use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable;
17 use Civi\Api4\Utils\FormattingUtil;
18 use Civi\Api4\Utils\CoreUtil;
19
20 /**
21 * @method string getLanguage()
22 * @method $this setLanguage(string $language)
23 */
24 trait DAOActionTrait {
25
26 /**
27 * Specify the language to use if this is a multi-lingual environment.
28 *
29 * E.g. "en_US" or "fr_CA"
30 *
31 * @var string
32 */
33 protected $language;
34
35 /**
36 * @return \CRM_Core_DAO|string
37 */
38 protected function getBaoName() {
39 return CoreUtil::getBAOFromApiName($this->getEntityName());
40 }
41
42 /**
43 * Convert saved object to array
44 *
45 * Used by create, update & save actions
46 *
47 * @param \CRM_Core_DAO $bao
48 * @param array $input
49 * @return array
50 */
51 public function baoToArray($bao, $input) {
52 $allFields = array_column($bao->fields(), 'name');
53 if (!empty($this->reload)) {
54 $inputFields = $allFields;
55 $bao->find(TRUE);
56 }
57 else {
58 $inputFields = array_keys($input);
59 // Convert 'null' input to true null
60 foreach ($input as $key => $val) {
61 if ($val === 'null') {
62 $bao->$key = NULL;
63 }
64 }
65 }
66 $values = [];
67 foreach ($allFields as $field) {
68 if (isset($bao->$field) || in_array($field, $inputFields)) {
69 $values[$field] = $bao->$field ?? NULL;
70 }
71 }
72 return $values;
73 }
74
75 /**
76 * Fill field defaults which were declared by the api.
77 *
78 * Note: default values from core are ignored because the BAO or database layer will supply them.
79 *
80 * @param array $params
81 */
82 protected function fillDefaults(&$params) {
83 $fields = $this->entityFields();
84 $bao = $this->getBaoName();
85 $coreFields = array_column($bao::fields(), NULL, 'name');
86
87 foreach ($fields as $name => $field) {
88 // If a default value in the api field is different than in core, the api should override it.
89 if (!isset($params[$name]) && !empty($field['default_value']) && $field['default_value'] != \CRM_Utils_Array::pathGet($coreFields, [$name, 'default'])) {
90 $params[$name] = $field['default_value'];
91 }
92 }
93 }
94
95 /**
96 * Write bao objects as part of a create/update action.
97 *
98 * @param array $items
99 * The records to write to the DB.
100 *
101 * @return array
102 * The records after being written to the DB (e.g. including newly assigned "id").
103 * @throws \API_Exception
104 * @throws \CRM_Core_Exception
105 */
106 protected function writeObjects(&$items) {
107 $baoName = $this->getBaoName();
108
109 // TODO: Opt-in more entities to use the new writeRecords BAO method.
110 $functionNames = [
111 'Address' => 'add',
112 'CustomField' => 'writeRecords',
113 'EntityTag' => 'add',
114 'GroupContact' => 'add',
115 ];
116 $method = $functionNames[$this->getEntityName()] ?? NULL;
117 if (!isset($method)) {
118 $method = method_exists($baoName, 'create') ? 'create' : (method_exists($baoName, 'add') ? 'add' : 'writeRecords');
119 }
120
121 $result = [];
122
123 foreach ($items as &$item) {
124 $entityId = $item['id'] ?? NULL;
125 FormattingUtil::formatWriteParams($item, $this->entityFields());
126 $this->formatCustomParams($item, $entityId);
127
128 // Skip individual processing if using writeRecords
129 if ($method === 'writeRecords') {
130 continue;
131 }
132 $item['check_permissions'] = $this->getCheckPermissions();
133
134 // For some reason the contact bao requires this
135 if ($entityId && $this->getEntityName() === 'Contact') {
136 $item['contact_id'] = $entityId;
137 }
138
139 if ($this->getEntityName() === 'Address') {
140 $createResult = $baoName::$method($item, $this->fixAddress);
141 }
142 else {
143 $createResult = $baoName::$method($item);
144 }
145
146 if (!$createResult) {
147 $errMessage = sprintf('%s write operation failed', $this->getEntityName());
148 throw new \API_Exception($errMessage);
149 }
150
151 $result[] = $this->baoToArray($createResult, $item);
152 \CRM_Utils_API_HTMLInputCoder::singleton()->decodeRows($result);
153 }
154
155 // Use bulk `writeRecords` method if the BAO doesn't have a create or add method
156 // TODO: reverse this from opt-in to opt-out and default to using `writeRecords` for all BAOs
157 if ($method === 'writeRecords') {
158 $items = array_values($items);
159 foreach ($baoName::writeRecords($items) as $i => $createResult) {
160 $result[] = $this->baoToArray($createResult, $items[$i]);
161 }
162 }
163
164 FormattingUtil::formatOutputValues($result, $this->entityFields());
165 return $result;
166 }
167
168 /**
169 * @inheritDoc
170 */
171 protected function formatWriteValues(&$record) {
172 $this->resolveFKValues($record);
173 parent::formatWriteValues($record);
174 }
175
176 /**
177 * Looks up an id based on some other property of an fk entity
178 *
179 * @param array $record
180 */
181 private function resolveFKValues(array &$record): void {
182 foreach ($record as $key => $value) {
183 if (substr_count($key, '.') !== 1) {
184 continue;
185 }
186 [$fieldName, $fkField] = explode('.', $key);
187 $field = $this->entityFields()[$fieldName] ?? NULL;
188 if (!$field || empty($field['fk_entity'])) {
189 continue;
190 }
191 $fkDao = CoreUtil::getBAOFromApiName($field['fk_entity']);
192 $record[$fieldName] = \CRM_Core_DAO::getFieldValue($fkDao, $value, 'id', $fkField);
193 unset($record[$key]);
194 }
195 }
196
197 /**
198 * Converts params from flat array e.g. ['GroupName.Fieldname' => value] to the
199 * hierarchy expected by the BAO, nested within $params['custom'].
200 *
201 * @param array $params
202 * @param int $entityId
203 *
204 * @throws \API_Exception
205 * @throws \CRM_Core_Exception
206 */
207 protected function formatCustomParams(&$params, $entityId) {
208 $customParams = [];
209
210 foreach ($params as $name => $value) {
211 $field = $this->getCustomFieldInfo($name);
212 if (!$field) {
213 continue;
214 }
215
216 // todo are we sure we don't want to allow setting to NULL? need to test
217 if (NULL !== $value) {
218
219 if ($field['suffix']) {
220 $options = FormattingUtil::getPseudoconstantList($field, $name, $params, $this->getActionName());
221 $value = FormattingUtil::replacePseudoconstant($options, $value, TRUE);
222 }
223
224 if ($field['html_type'] === 'CheckBox') {
225 // this function should be part of a class
226 formatCheckBoxField($value, 'custom_' . $field['id'], $this->getEntityName());
227 }
228
229 // Match contact id to strings like "user_contact_id"
230 // FIXME handle arrays for multi-value contact reference fields, etc.
231 if ($field['data_type'] === 'ContactReference' && is_string($value) && !is_numeric($value)) {
232 // FIXME decouple from v3 API
233 require_once 'api/v3/utils.php';
234 $value = \_civicrm_api3_resolve_contactID($value);
235 if ('unknown-user' === $value) {
236 throw new \API_Exception("\"{$field['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $field['name'], "type" => "integer"]);
237 }
238 }
239
240 \CRM_Core_BAO_CustomField::formatCustomField(
241 $field['id'],
242 $customParams,
243 $value,
244 $field['custom_group_id.extends'],
245 // todo check when this is needed
246 NULL,
247 $entityId,
248 FALSE,
249 $this->getCheckPermissions(),
250 TRUE
251 );
252 }
253 }
254
255 $params['custom'] = $customParams ?: NULL;
256 }
257
258 /**
259 * Gets field info needed to save custom data
260 *
261 * @param string $fieldExpr
262 * Field identifier with possible suffix, e.g. MyCustomGroup.MyField1:label
263 * @return array{id: int, name: string, entity: string, suffix: string, html_type: string, data_type: string}|NULL
264 */
265 protected function getCustomFieldInfo(string $fieldExpr) {
266 if (strpos($fieldExpr, '.') === FALSE) {
267 return NULL;
268 }
269 [$groupName, $fieldName] = explode('.', $fieldExpr);
270 [$fieldName, $suffix] = array_pad(explode(':', $fieldName), 2, NULL);
271 $cacheKey = "APIv4_Custom_Fields-$groupName";
272 $info = \Civi::cache('metadata')->get($cacheKey);
273 if (!isset($info[$fieldName])) {
274 $info = [];
275 $fields = CustomField::get(FALSE)
276 ->addSelect('id', 'name', 'html_type', 'data_type', 'custom_group_id.extends')
277 ->addWhere('custom_group_id.name', '=', $groupName)
278 ->execute()->indexBy('name');
279 foreach ($fields as $name => $field) {
280 $field['custom_field_id'] = $field['id'];
281 $field['name'] = $groupName . '.' . $name;
282 $field['entity'] = CustomGroupJoinable::getEntityFromExtends($field['custom_group_id.extends']);
283 $info[$name] = $field;
284 }
285 \Civi::cache('metadata')->set($cacheKey, $info);
286 }
287 return isset($info[$fieldName]) ? ['suffix' => $suffix] + $info[$fieldName] : NULL;
288 }
289
290 }