Merge pull request #23895 from colemanw/searchKitManaged
[civicrm-core.git] / CRM / Core / BAO / CustomValueTable.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 */
17class CRM_Core_BAO_CustomValueTable {
18
b5c2afd0 19 /**
100fef9d 20 * @param array $customParams
46fe0a66 21 * @param string $parentOperation Operation being taken on the parent entity.
22 * If we know the parent entity is doing an insert we can skip the
23 * ON DUPLICATE UPDATE - which improves performance and reduces deadlocks.
24 * - edit
25 * - create
b5c2afd0
EM
26 *
27 * @throws Exception
28 */
46fe0a66 29 public static function create($customParams, $parentOperation = NULL) {
6a488035
TO
30 if (empty($customParams) ||
31 !is_array($customParams)
32 ) {
33 return;
34 }
8ef12e64 35
be2fb01f 36 $paramFieldsExtendContactForEntities = [];
a61d2ab7 37 $VS = CRM_Core_DAO::VALUE_SEPARATOR;
63b7d442 38
6a488035 39 foreach ($customParams as $tableName => $tables) {
3d89c1a4 40 foreach ($tables as $fields) {
353ffa53 41 $hookID = NULL;
353ffa53 42 $entityID = NULL;
be2fb01f
CW
43 $set = [];
44 $params = [];
353ffa53 45 $count = 1;
6a488035 46
048b069c 47 $firstField = reset($fields);
445bbeed 48 $entityID = (int) $firstField['entity_id'];
048b069c
BS
49 $isMultiple = $firstField['is_multiple'];
50 if (array_key_exists('id', $firstField)) {
51 $sqlOP = "UPDATE $tableName ";
52 $where = " WHERE id = %{$count}";
53 $params[$count] = [$firstField['id'], 'Integer'];
54 $count++;
55 $hookOP = 'edit';
56 }
57 else {
58 $sqlOP = "INSERT INTO $tableName ";
59 $where = NULL;
60 $hookOP = 'create';
61 }
62
445bbeed
EM
63 CRM_Utils_Hook::customPre(
64 $hookOP,
65 (int) $firstField['custom_group_id'],
048b069c
BS
66 $entityID,
67 $fields
68 );
69
70 foreach ($fields as $field) {
6a488035
TO
71 // fix the value before we store it
72 $value = $field['value'];
73 $type = $field['type'];
74 switch ($type) {
75 case 'StateProvince':
76 $type = 'Integer';
77 if (is_array($value)) {
78 $value = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $value) . CRM_Core_DAO::VALUE_SEPARATOR;
79 $type = 'String';
80 }
ec28b24d 81 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
6a488035
TO
82 //fix for multi select state, CRM-3437
83 $mulValues = explode(',', $value);
be2fb01f 84 $validStates = [];
6a488035 85 foreach ($mulValues as $key => $stateVal) {
be2fb01f 86 $states = [];
6a488035
TO
87 $states['state_province'] = trim($stateVal);
88
89 CRM_Utils_Array::lookupValue($states, 'state_province',
90 CRM_Core_PseudoConstant::stateProvince(), TRUE
91 );
a7488080 92 if (empty($states['state_province_id'])) {
6a488035
TO
93 CRM_Utils_Array::lookupValue($states, 'state_province',
94 CRM_Core_PseudoConstant::stateProvinceAbbreviation(), TRUE
95 );
96 }
9c1bc317 97 $validStates[] = $states['state_province_id'] ?? NULL;
6a488035
TO
98 }
99 $value = implode(CRM_Core_DAO::VALUE_SEPARATOR,
100 $validStates
101 );
102 $type = 'String';
103 }
104 elseif (!$value) {
105 // CRM-3415
106 // using type of timestamp allows us to sneak in a null into db
107 // gross but effective hack
108 $value = NULL;
109 $type = 'Timestamp';
110 }
ec28b24d 111 else {
112 $type = 'String';
113 }
6a488035
TO
114 break;
115
116 case 'Country':
117 $type = 'Integer';
118 if (is_array($value)) {
119 $value = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $value) . CRM_Core_DAO::VALUE_SEPARATOR;
120 $type = 'String';
121 }
ec28b24d 122 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
6a488035
TO
123 //fix for multi select country, CRM-3437
124 $mulValues = explode(',', $value);
be2fb01f 125 $validCountries = [];
6a488035 126 foreach ($mulValues as $key => $countryVal) {
be2fb01f 127 $countries = [];
6a488035
TO
128 $countries['country'] = trim($countryVal);
129 CRM_Utils_Array::lookupValue($countries, 'country',
130 CRM_Core_PseudoConstant::country(), TRUE
131 );
a7488080 132 if (empty($countries['country_id'])) {
6a488035
TO
133 CRM_Utils_Array::lookupValue($countries, 'country',
134 CRM_Core_PseudoConstant::countryIsoCode(), TRUE
135 );
136 }
9c1bc317 137 $validCountries[] = $countries['country_id'] ?? NULL;
6a488035
TO
138 }
139 $value = implode(CRM_Core_DAO::VALUE_SEPARATOR,
140 $validCountries
141 );
142 $type = 'String';
143 }
144 elseif (!$value) {
145 // CRM-3415
146 // using type of timestamp allows us to sneak in a null into db
147 // gross but effective hack
148 $value = NULL;
149 $type = 'Timestamp';
150 }
ec28b24d 151 else {
152 $type = 'String';
153 }
6a488035
TO
154 break;
155
156 case 'File':
157 if (!$field['file_id']) {
9d85626b
CW
158 $value = 'null';
159 break;
6a488035
TO
160 }
161
162 // need to add/update civicrm_entity_file
163 $entityFileDAO = new CRM_Core_DAO_EntityFile();
164 $entityFileDAO->file_id = $field['file_id'];
165 $entityFileDAO->find(TRUE);
166
167 $entityFileDAO->entity_table = $field['table_name'];
168 $entityFileDAO->entity_id = $field['entity_id'];
169 $entityFileDAO->file_id = $field['file_id'];
170 $entityFileDAO->save();
6a488035
TO
171 $value = $field['file_id'];
172 $type = 'String';
173 break;
174
175 case 'Date':
176 $value = CRM_Utils_Date::isoToMysql($value);
177 break;
178
179 case 'Int':
180 if (is_numeric($value)) {
181 $type = 'Integer';
182 }
183 else {
184 $type = 'Timestamp';
185 }
186 break;
187
188 case 'ContactReference':
a61d2ab7 189 if ($value == NULL || $value === '' || $value === $VS . $VS) {
6a488035 190 $type = 'Timestamp';
a61d2ab7
CW
191 $value = NULL;
192 }
193 elseif (strpos($value, $VS) !== FALSE) {
194 $type = 'String';
195 // Validate the string contains only integers and value-separators
196 $validChars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, $VS];
197 if (str_replace($validChars, '', $value)) {
198 throw new CRM_Core_Exception('Contact ID must be of type Integer');
199 }
6a488035
TO
200 }
201 else {
202 $type = 'Integer';
203 }
204 break;
205
206 case 'RichTextEditor':
207 $type = 'String';
208 break;
209
210 case 'Boolean':
211 //fix for CRM-3290
212 $value = CRM_Utils_String::strtoboolstr($value);
213 if ($value === FALSE) {
214 $type = 'Timestamp';
215 }
216 break;
217
218 default:
219 break;
220 }
52fb3f74 221 if ($value === 'null') {
fd630ef9 222 // when unsetting a value to null, we don't need to validate the type
223 // https://projectllr.atlassian.net/browse/VGQBMP-20
224 $set[$field['column_name']] = $value;
f931b74c 225 }
226 else {
fd630ef9 227 $set[$field['column_name']] = "%{$count}";
d532d0cf 228 // The second parameter is the type of the db field, which
229 // would be 'String' for a concatenated set of integers.
831545ee 230 // However, the god-forsaken timestamp hack also needs to be kept
231 // if value is NULL.
b99d7139 232 $params[$count] = [$value, ($value && $field['serialize']) ? 'String' : $type];
fd630ef9 233 $count++;
234 }
63b7d442 235
9c1bc317 236 $fieldExtends = $field['extends'] ?? NULL;
63b7d442 237 if (
3d89c1a4
EM
238 CRM_Utils_Array::value('entity_table', $field) === 'civicrm_contact'
239 || $fieldExtends === 'Contact'
240 || $fieldExtends === 'Individual'
241 || $fieldExtends === 'Organization'
242 || $fieldExtends === 'Household'
63b7d442 243 ) {
9c1bc317 244 $paramFieldsExtendContactForEntities[$entityID]['custom_' . CRM_Utils_Array::value('custom_field_id', $field)] = $field['custom_field_id'] ?? NULL;
63b7d442 245 }
6a488035
TO
246 }
247
248 if (!empty($set)) {
be2fb01f 249 $setClause = [];
6a488035 250 foreach ($set as $n => $v) {
4f426b76 251 $setClause[] = "`$n` = $v";
6a488035
TO
252 }
253 $setClause = implode(',', $setClause);
254 if (!$where) {
255 // do this only for insert
256 $set['entity_id'] = "%{$count}";
be2fb01f 257 $params[$count] = [$entityID, 'Integer'];
6a488035
TO
258 $count++;
259
65aae70e 260 $fieldNames = implode(',', CRM_Utils_Type::escapeAll(array_keys($set), 'MysqlColumnNameOrAlias'));
6a488035 261 $fieldValues = implode(',', array_values($set));
353ffa53 262 $query = "$sqlOP ( $fieldNames ) VALUES ( $fieldValues )";
6a488035 263 // for multiple values we dont do on duplicate key update
46fe0a66 264 if (!$isMultiple && $parentOperation !== 'create') {
6a488035
TO
265 $query .= " ON DUPLICATE KEY UPDATE $setClause";
266 }
267 }
268 else {
269 $query = "$sqlOP SET $setClause $where";
270 }
d532d0cf 271 CRM_Core_DAO::executeQuery($query, $params);
6a488035
TO
272
273 CRM_Utils_Hook::custom($hookOP,
445bbeed 274 (int) $firstField['custom_group_id'],
6a488035
TO
275 $entityID,
276 $fields
277 );
278 }
279 }
280 }
63b7d442
AS
281
282 if (!empty($paramFieldsExtendContactForEntities)) {
be2fb01f 283 CRM_Contact_BAO_Contact::updateGreetingsOnTokenFieldChange($paramFieldsExtendContactForEntities, ['contact_id' => $entityID]);
63b7d442 284 }
6a488035
TO
285 }
286
287 /**
fe482240 288 * Given a field return the mysql data type associated with it.
6a488035 289 *
6a0b768e 290 * @param string $type
fd31fa4c
EM
291 * @param int $maxLength
292 *
72b3a70c
CW
293 * @return string
294 * the mysql data store placeholder
6a488035
TO
295 */
296 public static function fieldToSQLType($type, $maxLength = 255) {
297 if (!isset($maxLength) ||
298 !is_numeric($maxLength) ||
299 $maxLength <= 0
300 ) {
301 $maxLength = 255;
302 }
303
304 switch ($type) {
305 case 'String':
306 case 'Link':
307 return "varchar($maxLength)";
308
309 case 'Boolean':
310 return 'tinyint';
311
312 case 'Int':
313 return 'int';
2aa397bc 314
6a488035
TO
315 // the below three are FK's, and have constraints added to them
316
317 case 'ContactReference':
318 case 'StateProvince':
319 case 'Country':
320 case 'File':
321 return 'int unsigned';
322
323 case 'Float':
324 return 'double';
325
326 case 'Money':
327 return 'decimal(20,2)';
328
329 case 'Memo':
330 case 'RichTextEditor':
331 return 'text';
332
333 case 'Date':
334 return 'datetime';
335
336 default:
ac15829d 337 throw new CRM_Core_Exception('Invalid Field Type');
6a488035
TO
338 }
339 }
340
b5c2afd0 341 /**
c490a46a 342 * @param array $params
b5c2afd0 343 * @param $entityTable
100fef9d 344 * @param int $entityID
46fe0a66 345 * @param string $parentOperation Operation being taken on the parent entity.
346 * If we know the parent entity is doing an insert we can skip the
347 * ON DUPLICATE UPDATE - which improves performance and reduces deadlocks.
348 * - edit
349 * - create
b5c2afd0 350 */
46fe0a66 351 public static function store($params, $entityTable, $entityID, $parentOperation = NULL) {
be2fb01f 352 $cvParams = [];
6a488035
TO
353 foreach ($params as $fieldID => $param) {
354 foreach ($param as $index => $customValue) {
be2fb01f 355 $cvParam = [
6a488035
TO
356 'entity_table' => $entityTable,
357 'entity_id' => $entityID,
358 'value' => $customValue['value'],
359 'type' => $customValue['type'],
360 'custom_field_id' => $customValue['custom_field_id'],
361 'custom_group_id' => $customValue['custom_group_id'],
362 'table_name' => $customValue['table_name'],
363 'column_name' => $customValue['column_name'],
b99d7139 364 // is_multiple refers to the custom group, serialize refers to the field.
365 // @todo is_multiple can be null - does that mean anything different from 0?
366 'is_multiple' => $customValue['is_multiple'] ?? CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customValue['custom_group_id'], 'is_multiple'),
367 'serialize' => $customValue['serialize'] ?? (int) CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', $customValue['custom_field_id'], 'serialize'),
6a488035 368 'file_id' => $customValue['file_id'],
be2fb01f 369 ];
6a488035 370
fe482240 371 // Fix Date type to be timestamp, since that is how we store in db.
6a488035
TO
372 if ($cvParam['type'] == 'Date') {
373 $cvParam['type'] = 'Timestamp';
374 }
375
a7488080 376 if (!empty($customValue['id'])) {
6a488035
TO
377 $cvParam['id'] = $customValue['id'];
378 }
a1fc8261
SL
379 elseif (empty($cvParam['is_multiple']) && !empty($entityID)) {
380 // dev/core#3000 Ensure that if we are not dealing with multiple record custom data and for some reason have got here without getting the id of the record in the custom table for this entityId let us give it one last shot
381 $rowId = CRM_Core_DAO::singleValueQuery("SELECT id FROM {$cvParam['table_name']} WHERE entity_id = %1", [1 => [$entityID, 'Integer']]);
382 if (!empty($rowId)) {
383 $cvParam['id'] = $rowId;
384 }
385 }
6a488035 386 if (!array_key_exists($customValue['table_name'], $cvParams)) {
be2fb01f 387 $cvParams[$customValue['table_name']] = [];
6a488035
TO
388 }
389
390 if (!array_key_exists($index, $cvParams[$customValue['table_name']])) {
be2fb01f 391 $cvParams[$customValue['table_name']][$index] = [];
6a488035
TO
392 }
393
394 $cvParams[$customValue['table_name']][$index][] = $cvParam;
395 }
396 }
397 if (!empty($cvParams)) {
46fe0a66 398 self::create($cvParams, $parentOperation);
6a488035
TO
399 }
400 }
401
b5c2afd0 402 /**
fe482240
EM
403 * Post process function.
404 *
c490a46a 405 * @param array $params
b5c2afd0 406 * @param $entityTable
100fef9d 407 * @param int $entityID
b5c2afd0 408 * @param $customFieldExtends
11201e17 409 * @param $parentOperation
b5c2afd0 410 */
11201e17 411 public static function postProcess(&$params, $entityTable, $entityID, $customFieldExtends, $parentOperation = NULL) {
6a488035 412 $customData = CRM_Core_BAO_CustomField::postProcess($params,
6a488035
TO
413 $entityID,
414 $customFieldExtends
415 );
416
417 if (!empty($customData)) {
11201e17 418 self::store($customData, $entityTable, $entityID, $parentOperation);
6a488035
TO
419 }
420 }
421
422 /**
423 * Return an array of all custom values associated with an entity.
424 *
6a0b768e
TO
425 * @param int $entityID
426 * Identification number of the entity.
427 * @param string $entityType
428 * Type of entity that the entityID corresponds to, specified.
6a488035
TO
429 * as a string with format "'<EntityName>'". Comma separated
430 * list may be used to specify OR matches. Allowable values
431 * are enumerated types in civicrm_custom_group.extends field.
432 * Optional. Default value assumes entityID references a
433 * contact entity.
6a0b768e
TO
434 * @param array $fieldIDs
435 * Optional list of fieldIDs that we want to retrieve. If this.
6a488035
TO
436 * is set the entityType is ignored
437 *
77b97be7 438 * @param bool $formatMultiRecordField
c693f065 439 * @param array $DTparams - CRM-17810 dataTable params for the multiValued custom fields.
77b97be7 440 *
a6c01b45
CW
441 * @return array
442 * Array of custom values for the entity with key=>value
6a488035
TO
443 * pairs specified as civicrm_custom_field.id => custom value.
444 * Empty array if no custom values found.
ac15829d 445 * @throws CRM_Core_Exception
6a488035 446 */
f1b78592 447 public static function getEntityValues($entityID, $entityType = NULL, $fieldIDs = NULL, $formatMultiRecordField = FALSE, $DTparams = NULL) {
6a488035
TO
448 if (!$entityID) {
449 // adding this here since an empty contact id could have serious repurcussions
450 // like looping forever
ac15829d 451 throw new CRM_Core_Exception('Please file an issue with the backtrace');
6a488035
TO
452 return NULL;
453 }
454
be2fb01f 455 $cond = [];
6a488035
TO
456 if ($entityType) {
457 $cond[] = "cg.extends IN ( '$entityType' )";
458 }
459 if ($fieldIDs &&
460 is_array($fieldIDs)
461 ) {
462 $fieldIDList = implode(',', $fieldIDs);
463 $cond[] = "cf.id IN ( $fieldIDList )";
464 }
465 if (empty($cond)) {
f1b78592
CW
466 $contactTypes = array_merge(['Contact'], CRM_Contact_BAO_ContactType::basicTypes(TRUE));
467 $cond[] = "cg.extends IN ( '" . implode("', '", $contactTypes) . "' )";
6a488035
TO
468 }
469 $cond = implode(' AND ', $cond);
470
e87c8fb7 471 $limit = $orderBy = '';
c693f065 472 if (!empty($DTparams['rowCount']) && $DTparams['rowCount'] > 0) {
d51e02d3 473 $limit = " LIMIT " . CRM_Utils_Type::escape($DTparams['offset'], 'Integer') . ", " . CRM_Utils_Type::escape($DTparams['rowCount'], 'Integer');
c693f065 474 }
c693f065 475 if (!empty($DTparams['sort'])) {
476 $orderBy = ' ORDER BY ' . CRM_Utils_Type::escape($DTparams['sort'], 'String');
477 }
478
fe482240 479 // First find all the fields that extend this type of entity.
6a488035
TO
480 $query = "
481SELECT cg.table_name,
482 cg.id as groupID,
483 cg.is_multiple,
484 cf.column_name,
34f51a07
N
485 cf.id as fieldID,
486 cf.data_type as fieldDataType
6a488035
TO
487FROM civicrm_custom_group cg,
488 civicrm_custom_field cf
489WHERE cf.custom_group_id = cg.id
490AND cg.is_active = 1
491AND cf.is_active = 1
492AND $cond
493";
494 $dao = CRM_Core_DAO::executeQuery($query);
495
be2fb01f 496 $select = $fields = $isMultiple = [];
6a488035
TO
497
498 while ($dao->fetch()) {
499 if (!array_key_exists($dao->table_name, $select)) {
be2fb01f
CW
500 $fields[$dao->table_name] = [];
501 $select[$dao->table_name] = [];
6a488035
TO
502 }
503 $fields[$dao->table_name][] = $dao->fieldID;
504 $select[$dao->table_name][] = "{$dao->column_name} AS custom_{$dao->fieldID}";
63d76404 505 $isMultiple[$dao->table_name] = (bool) $dao->is_multiple;
77b97be7 506 $file[$dao->table_name][$dao->fieldID] = $dao->fieldDataType;
6a488035
TO
507 }
508
be2fb01f 509 $result = $sortedResult = [];
6a488035 510 foreach ($select as $tableName => $clauses) {
1c66bdc7 511 if (!empty($DTparams['sort'])) {
512 $query = CRM_Core_DAO::executeQuery("SELECT id FROM {$tableName} WHERE entity_id = {$entityID}");
513 $count = 1;
514 while ($query->fetch()) {
515 $sortedResult["{$query->id}"] = $count;
516 $count++;
517 }
518 }
519
c693f065 520 $query = "SELECT SQL_CALC_FOUND_ROWS id, " . implode(', ', $clauses) . " FROM $tableName WHERE entity_id = $entityID {$orderBy} {$limit}";
6a488035 521 $dao = CRM_Core_DAO::executeQuery($query);
e87c8fb7 522 if (!empty($DTparams)) {
523 $result['count'] = CRM_Core_DAO::singleValueQuery('SELECT FOUND_ROWS()');
524 }
6a488035
TO
525 while ($dao->fetch()) {
526 foreach ($fields[$tableName] as $fieldID) {
527 $fieldName = "custom_{$fieldID}";
528 if ($isMultiple[$tableName]) {
529 if ($formatMultiRecordField) {
d8f34a6e 530 $result["{$dao->id}"]["{$fieldID}"] = $dao->$fieldName;
fe482240
EM
531 }
532 else {
6a488035
TO
533 $result["{$fieldID}_{$dao->id}"] = $dao->$fieldName;
534 }
535 }
536 else {
d8f34a6e 537 $result[$fieldID] = $dao->$fieldName;
6a488035
TO
538 }
539 }
540 }
541 }
e525d6af 542 if (!empty($sortedResult)) {
543 $result['sortedResult'] = $sortedResult;
1c66bdc7 544 }
6a488035
TO
545 return $result;
546 }
547
548 /**
100fef9d 549 * Take in an array of entityID, custom_XXX => value
6a488035
TO
550 * and set the value in the appropriate table. Should also be able
551 * to set the value to null. Follows api parameter/return conventions
552 *
553 * @array $params
554 *
c490a46a 555 * @param array $params
2a6da8d7
EM
556 *
557 * @throws Exception
6a488035 558 * @return array
6a488035 559 */
00be9182 560 public static function setValues(&$params) {
d1b0ffad
CW
561 // For legacy reasons, accept this param in either format
562 if (empty($params['entityID']) && !empty($params['entity_id'])) {
563 $params['entityID'] = $params['entity_id'];
564 }
6a488035 565
d1b0ffad 566 if (!isset($params['entityID']) || !CRM_Utils_Type::validate($params['entityID'], 'Integer', FALSE)) {
e31fc637 567 throw new CRM_Core_Exception(ts('entity_id needs to be set and of type Integer'));
6a488035
TO
568 }
569
570 // first collect all the id/value pairs. The format is:
571 // custom_X => value or custom_X_VALUEID => value (for multiple values), VALUEID == -1, -2 etc for new insertions
be2fb01f 572 $fieldValues = [];
6a488035
TO
573 foreach ($params as $n => $v) {
574 if ($customFieldInfo = CRM_Core_BAO_CustomField::getKeyID($n, TRUE)) {
575 $fieldID = (int ) $customFieldInfo[0];
576 if (CRM_Utils_Type::escape($fieldID, 'Integer', FALSE) === NULL) {
e31fc637 577 throw new CRM_Core_Exception(ts('field ID needs to be of type Integer for index %1',
be2fb01f 578 [1 => $fieldID]
353ffa53 579 ));
6a488035
TO
580 }
581 if (!array_key_exists($fieldID, $fieldValues)) {
be2fb01f 582 $fieldValues[$fieldID] = [];
6a488035
TO
583 }
584 $id = -1;
585 if ($customFieldInfo[1]) {
586 $id = (int ) $customFieldInfo[1];
587 }
be2fb01f 588 $fieldValues[$fieldID][] = [
6a488035
TO
589 'value' => $v,
590 'id' => $id,
be2fb01f 591 ];
6a488035
TO
592 }
593 }
594
595 $fieldIDList = implode(',', array_keys($fieldValues));
596
597 // format it so that we can just use create
598 $sql = "
599SELECT cg.table_name as table_name ,
600 cg.id as cg_id ,
601 cg.is_multiple as is_multiple,
63b7d442 602 cg.extends as extends,
6a488035
TO
603 cf.column_name as column_name,
604 cf.id as cf_id ,
3766bd36 605 cf.html_type as html_type ,
b99d7139 606 cf.data_type as data_type ,
607 cf.serialize as serialize
6a488035
TO
608FROM civicrm_custom_group cg,
609 civicrm_custom_field cf
610WHERE cf.custom_group_id = cg.id
611AND cf.id IN ( $fieldIDList )
612";
613
614 $dao = CRM_Core_DAO::executeQuery($sql);
be2fb01f 615 $cvParams = [];
6a488035
TO
616
617 while ($dao->fetch()) {
618 $dataType = $dao->data_type == 'Date' ? 'Timestamp' : $dao->data_type;
619 foreach ($fieldValues[$dao->cf_id] as $fieldValue) {
3766bd36
CW
620 // Serialize array values
621 if (is_array($fieldValue['value']) && CRM_Core_BAO_CustomField::isSerialized($dao)) {
622 $fieldValue['value'] = CRM_Utils_Array::implodePadded($fieldValue['value']);
623 }
6a488035
TO
624 // Format null values correctly
625 if ($fieldValue['value'] === NULL || $fieldValue['value'] === '') {
626 switch ($dataType) {
627 case 'String':
628 case 'Int':
629 case 'Link':
630 case 'Boolean':
631 $fieldValue['value'] = '';
632 break;
633
634 case 'Timestamp':
635 $fieldValue['value'] = NULL;
636 break;
637
638 case 'StateProvince':
639 case 'Country':
640 case 'Money':
641 case 'Float':
2aa397bc 642 $fieldValue['value'] = (int) 0;
6a488035
TO
643 break;
644 }
645 }
646 // Ensure that value is of the right data type
647 elseif (CRM_Utils_Type::escape($fieldValue['value'], $dataType, FALSE) === NULL) {
e31fc637 648 throw new CRM_Core_Exception(ts('value: %1 is not of the right field data type: %2',
be2fb01f 649 [
353ffa53
TO
650 1 => $fieldValue['value'],
651 2 => $dao->data_type,
be2fb01f 652 ]
353ffa53 653 ));
6a488035
TO
654 }
655
be2fb01f 656 $cvParam = [
6a488035
TO
657 'entity_id' => $params['entityID'],
658 'value' => $fieldValue['value'],
659 'type' => $dataType,
660 'custom_field_id' => $dao->cf_id,
661 'custom_group_id' => $dao->cg_id,
662 'table_name' => $dao->table_name,
663 'column_name' => $dao->column_name,
664 'is_multiple' => $dao->is_multiple,
b99d7139 665 'serialize' => $dao->serialize,
63b7d442 666 'extends' => $dao->extends,
be2fb01f 667 ];
6a488035 668
5013100c 669 if (!empty($params['id'])) {
670 $cvParam['id'] = $params['id'];
671 }
672
e7dcccf0
CW
673 if ($cvParam['type'] == 'File') {
674 $cvParam['file_id'] = $fieldValue['value'];
675 }
676
6a488035 677 if (!array_key_exists($dao->table_name, $cvParams)) {
be2fb01f 678 $cvParams[$dao->table_name] = [];
6a488035
TO
679 }
680
681 if (!array_key_exists($fieldValue['id'], $cvParams[$dao->table_name])) {
be2fb01f 682 $cvParams[$dao->table_name][$fieldValue['id']] = [];
6a488035
TO
683 }
684
685 if ($fieldValue['id'] > 0) {
686 $cvParam['id'] = $fieldValue['id'];
687 }
688 $cvParams[$dao->table_name][$fieldValue['id']][] = $cvParam;
689 }
690 }
691
692 if (!empty($cvParams)) {
693 self::create($cvParams);
be2fb01f 694 return ['is_error' => 0, 'result' => 1];
6a488035
TO
695 }
696
e31fc637 697 throw new CRM_Core_Exception(ts('Unknown error'));
6a488035
TO
698 }
699
700 /**
100fef9d 701 * Take in an array of entityID, custom_ID
6a488035
TO
702 * and gets the value from the appropriate table.
703 *
704 * To get the values of custom fields with IDs 13 and 43 for contact ID 1327, use:
705 * $params = array( 'entityID' => 1327, 'custom_13' => 1, 'custom_43' => 1 );
706 *
b44e3f84 707 * Entity Type will be inferred by the custom fields you request
6a488035
TO
708 * Specify $params['entityType'] if you do not supply any custom fields to return
709 * and entity type is other than Contact
710 *
711 * @array $params
712 *
c490a46a 713 * @param array $params
2a6da8d7
EM
714 *
715 * @throws Exception
6a488035 716 * @return array
6a488035 717 */
f1b78592 718 public static function getValues($params) {
6a488035
TO
719 if (empty($params)) {
720 return NULL;
721 }
722 if (!isset($params['entityID']) ||
723 CRM_Utils_Type::escape($params['entityID'],
724 'Integer', FALSE
725 ) === NULL
726 ) {
727 return CRM_Core_Error::createAPIError(ts('entityID needs to be set and of type Integer'));
728 }
729
730 // first collect all the ids. The format is:
731 // custom_ID
be2fb01f 732 $fieldIDs = [];
6a488035
TO
733 foreach ($params as $n => $v) {
734 $key = $idx = NULL;
735 if (substr($n, 0, 7) == 'custom_') {
736 $idx = substr($n, 7);
737 if (CRM_Utils_Type::escape($idx, 'Integer', FALSE) === NULL) {
738 return CRM_Core_Error::createAPIError(ts('field ID needs to be of type Integer for index %1',
be2fb01f 739 [1 => $idx]
353ffa53 740 ));
6a488035 741 }
f1b78592 742 $fieldIDs[] = (int) $idx;
6a488035
TO
743 }
744 }
745
f1b78592 746 $default = array_merge(['Contact'], CRM_Contact_BAO_ContactType::basicTypes(TRUE));
6a488035
TO
747 if (!($type = CRM_Utils_Array::value('entityType', $params)) ||
748 in_array($params['entityType'], $default)
749 ) {
750 $type = NULL;
751 }
752 else {
753 $entities = CRM_Core_SelectValues::customGroupExtends();
754 if (!array_key_exists($type, $entities)) {
755 if (in_array($type, $entities)) {
756 $type = $entities[$type];
757 if (in_array($type, $default)) {
758 $type = NULL;
759 }
760 }
761 else {
762 return CRM_Core_Error::createAPIError(ts('Invalid entity type') . ': "' . $type . '"');
763 }
764 }
765 }
766
767 $values = self::getEntityValues($params['entityID'],
768 $type,
769 $fieldIDs
770 );
771 if (empty($values)) {
772 // note that this behaviour is undesirable from an API point of view - it should return an empty array
773 // since this is also called by the merger code & not sure the consequences of changing
774 // are just handling undoing this in the api layer. ie. converting the error back into a success
be2fb01f 775 $result = [
6a488035
TO
776 'is_error' => 1,
777 'error_message' => 'No values found for the specified entity ID and custom field(s).',
be2fb01f 778 ];
6a488035
TO
779 return $result;
780 }
781 else {
be2fb01f 782 $result = [
6a488035
TO
783 'is_error' => 0,
784 'entityID' => $params['entityID'],
be2fb01f 785 ];
6a488035
TO
786 foreach ($values as $id => $value) {
787 $result["custom_{$id}"] = $value;
788 }
789 return $result;
790 }
791 }
96025800 792
6a488035 793}