Merge pull request #23895 from colemanw/searchKitManaged
[civicrm-core.git] / CRM / Core / BAO / CustomValueTable.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Core_BAO_CustomValueTable {
18
19 /**
20 * @param array $customParams
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
26 *
27 * @throws Exception
28 */
29 public static function create($customParams, $parentOperation = NULL) {
30 if (empty($customParams) ||
31 !is_array($customParams)
32 ) {
33 return;
34 }
35
36 $paramFieldsExtendContactForEntities = [];
37 $VS = CRM_Core_DAO::VALUE_SEPARATOR;
38
39 foreach ($customParams as $tableName => $tables) {
40 foreach ($tables as $fields) {
41 $hookID = NULL;
42 $entityID = NULL;
43 $set = [];
44 $params = [];
45 $count = 1;
46
47 $firstField = reset($fields);
48 $entityID = (int) $firstField['entity_id'];
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
63 CRM_Utils_Hook::customPre(
64 $hookOP,
65 (int) $firstField['custom_group_id'],
66 $entityID,
67 $fields
68 );
69
70 foreach ($fields as $field) {
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 }
81 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
82 //fix for multi select state, CRM-3437
83 $mulValues = explode(',', $value);
84 $validStates = [];
85 foreach ($mulValues as $key => $stateVal) {
86 $states = [];
87 $states['state_province'] = trim($stateVal);
88
89 CRM_Utils_Array::lookupValue($states, 'state_province',
90 CRM_Core_PseudoConstant::stateProvince(), TRUE
91 );
92 if (empty($states['state_province_id'])) {
93 CRM_Utils_Array::lookupValue($states, 'state_province',
94 CRM_Core_PseudoConstant::stateProvinceAbbreviation(), TRUE
95 );
96 }
97 $validStates[] = $states['state_province_id'] ?? NULL;
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 }
111 else {
112 $type = 'String';
113 }
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 }
122 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
123 //fix for multi select country, CRM-3437
124 $mulValues = explode(',', $value);
125 $validCountries = [];
126 foreach ($mulValues as $key => $countryVal) {
127 $countries = [];
128 $countries['country'] = trim($countryVal);
129 CRM_Utils_Array::lookupValue($countries, 'country',
130 CRM_Core_PseudoConstant::country(), TRUE
131 );
132 if (empty($countries['country_id'])) {
133 CRM_Utils_Array::lookupValue($countries, 'country',
134 CRM_Core_PseudoConstant::countryIsoCode(), TRUE
135 );
136 }
137 $validCountries[] = $countries['country_id'] ?? NULL;
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 }
151 else {
152 $type = 'String';
153 }
154 break;
155
156 case 'File':
157 if (!$field['file_id']) {
158 $value = 'null';
159 break;
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();
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':
189 if ($value == NULL || $value === '' || $value === $VS . $VS) {
190 $type = 'Timestamp';
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 }
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 }
221 if ($value === 'null') {
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;
225 }
226 else {
227 $set[$field['column_name']] = "%{$count}";
228 // The second parameter is the type of the db field, which
229 // would be 'String' for a concatenated set of integers.
230 // However, the god-forsaken timestamp hack also needs to be kept
231 // if value is NULL.
232 $params[$count] = [$value, ($value && $field['serialize']) ? 'String' : $type];
233 $count++;
234 }
235
236 $fieldExtends = $field['extends'] ?? NULL;
237 if (
238 CRM_Utils_Array::value('entity_table', $field) === 'civicrm_contact'
239 || $fieldExtends === 'Contact'
240 || $fieldExtends === 'Individual'
241 || $fieldExtends === 'Organization'
242 || $fieldExtends === 'Household'
243 ) {
244 $paramFieldsExtendContactForEntities[$entityID]['custom_' . CRM_Utils_Array::value('custom_field_id', $field)] = $field['custom_field_id'] ?? NULL;
245 }
246 }
247
248 if (!empty($set)) {
249 $setClause = [];
250 foreach ($set as $n => $v) {
251 $setClause[] = "`$n` = $v";
252 }
253 $setClause = implode(',', $setClause);
254 if (!$where) {
255 // do this only for insert
256 $set['entity_id'] = "%{$count}";
257 $params[$count] = [$entityID, 'Integer'];
258 $count++;
259
260 $fieldNames = implode(',', CRM_Utils_Type::escapeAll(array_keys($set), 'MysqlColumnNameOrAlias'));
261 $fieldValues = implode(',', array_values($set));
262 $query = "$sqlOP ( $fieldNames ) VALUES ( $fieldValues )";
263 // for multiple values we dont do on duplicate key update
264 if (!$isMultiple && $parentOperation !== 'create') {
265 $query .= " ON DUPLICATE KEY UPDATE $setClause";
266 }
267 }
268 else {
269 $query = "$sqlOP SET $setClause $where";
270 }
271 CRM_Core_DAO::executeQuery($query, $params);
272
273 CRM_Utils_Hook::custom($hookOP,
274 (int) $firstField['custom_group_id'],
275 $entityID,
276 $fields
277 );
278 }
279 }
280 }
281
282 if (!empty($paramFieldsExtendContactForEntities)) {
283 CRM_Contact_BAO_Contact::updateGreetingsOnTokenFieldChange($paramFieldsExtendContactForEntities, ['contact_id' => $entityID]);
284 }
285 }
286
287 /**
288 * Given a field return the mysql data type associated with it.
289 *
290 * @param string $type
291 * @param int $maxLength
292 *
293 * @return string
294 * the mysql data store placeholder
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';
314
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:
337 throw new CRM_Core_Exception('Invalid Field Type');
338 }
339 }
340
341 /**
342 * @param array $params
343 * @param $entityTable
344 * @param int $entityID
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
350 */
351 public static function store($params, $entityTable, $entityID, $parentOperation = NULL) {
352 $cvParams = [];
353 foreach ($params as $fieldID => $param) {
354 foreach ($param as $index => $customValue) {
355 $cvParam = [
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'],
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'),
368 'file_id' => $customValue['file_id'],
369 ];
370
371 // Fix Date type to be timestamp, since that is how we store in db.
372 if ($cvParam['type'] == 'Date') {
373 $cvParam['type'] = 'Timestamp';
374 }
375
376 if (!empty($customValue['id'])) {
377 $cvParam['id'] = $customValue['id'];
378 }
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 }
386 if (!array_key_exists($customValue['table_name'], $cvParams)) {
387 $cvParams[$customValue['table_name']] = [];
388 }
389
390 if (!array_key_exists($index, $cvParams[$customValue['table_name']])) {
391 $cvParams[$customValue['table_name']][$index] = [];
392 }
393
394 $cvParams[$customValue['table_name']][$index][] = $cvParam;
395 }
396 }
397 if (!empty($cvParams)) {
398 self::create($cvParams, $parentOperation);
399 }
400 }
401
402 /**
403 * Post process function.
404 *
405 * @param array $params
406 * @param $entityTable
407 * @param int $entityID
408 * @param $customFieldExtends
409 * @param $parentOperation
410 */
411 public static function postProcess(&$params, $entityTable, $entityID, $customFieldExtends, $parentOperation = NULL) {
412 $customData = CRM_Core_BAO_CustomField::postProcess($params,
413 $entityID,
414 $customFieldExtends
415 );
416
417 if (!empty($customData)) {
418 self::store($customData, $entityTable, $entityID, $parentOperation);
419 }
420 }
421
422 /**
423 * Return an array of all custom values associated with an entity.
424 *
425 * @param int $entityID
426 * Identification number of the entity.
427 * @param string $entityType
428 * Type of entity that the entityID corresponds to, specified.
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.
434 * @param array $fieldIDs
435 * Optional list of fieldIDs that we want to retrieve. If this.
436 * is set the entityType is ignored
437 *
438 * @param bool $formatMultiRecordField
439 * @param array $DTparams - CRM-17810 dataTable params for the multiValued custom fields.
440 *
441 * @return array
442 * Array of custom values for the entity with key=>value
443 * pairs specified as civicrm_custom_field.id => custom value.
444 * Empty array if no custom values found.
445 * @throws CRM_Core_Exception
446 */
447 public static function getEntityValues($entityID, $entityType = NULL, $fieldIDs = NULL, $formatMultiRecordField = FALSE, $DTparams = NULL) {
448 if (!$entityID) {
449 // adding this here since an empty contact id could have serious repurcussions
450 // like looping forever
451 throw new CRM_Core_Exception('Please file an issue with the backtrace');
452 return NULL;
453 }
454
455 $cond = [];
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)) {
466 $contactTypes = array_merge(['Contact'], CRM_Contact_BAO_ContactType::basicTypes(TRUE));
467 $cond[] = "cg.extends IN ( '" . implode("', '", $contactTypes) . "' )";
468 }
469 $cond = implode(' AND ', $cond);
470
471 $limit = $orderBy = '';
472 if (!empty($DTparams['rowCount']) && $DTparams['rowCount'] > 0) {
473 $limit = " LIMIT " . CRM_Utils_Type::escape($DTparams['offset'], 'Integer') . ", " . CRM_Utils_Type::escape($DTparams['rowCount'], 'Integer');
474 }
475 if (!empty($DTparams['sort'])) {
476 $orderBy = ' ORDER BY ' . CRM_Utils_Type::escape($DTparams['sort'], 'String');
477 }
478
479 // First find all the fields that extend this type of entity.
480 $query = "
481 SELECT cg.table_name,
482 cg.id as groupID,
483 cg.is_multiple,
484 cf.column_name,
485 cf.id as fieldID,
486 cf.data_type as fieldDataType
487 FROM civicrm_custom_group cg,
488 civicrm_custom_field cf
489 WHERE cf.custom_group_id = cg.id
490 AND cg.is_active = 1
491 AND cf.is_active = 1
492 AND $cond
493 ";
494 $dao = CRM_Core_DAO::executeQuery($query);
495
496 $select = $fields = $isMultiple = [];
497
498 while ($dao->fetch()) {
499 if (!array_key_exists($dao->table_name, $select)) {
500 $fields[$dao->table_name] = [];
501 $select[$dao->table_name] = [];
502 }
503 $fields[$dao->table_name][] = $dao->fieldID;
504 $select[$dao->table_name][] = "{$dao->column_name} AS custom_{$dao->fieldID}";
505 $isMultiple[$dao->table_name] = (bool) $dao->is_multiple;
506 $file[$dao->table_name][$dao->fieldID] = $dao->fieldDataType;
507 }
508
509 $result = $sortedResult = [];
510 foreach ($select as $tableName => $clauses) {
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
520 $query = "SELECT SQL_CALC_FOUND_ROWS id, " . implode(', ', $clauses) . " FROM $tableName WHERE entity_id = $entityID {$orderBy} {$limit}";
521 $dao = CRM_Core_DAO::executeQuery($query);
522 if (!empty($DTparams)) {
523 $result['count'] = CRM_Core_DAO::singleValueQuery('SELECT FOUND_ROWS()');
524 }
525 while ($dao->fetch()) {
526 foreach ($fields[$tableName] as $fieldID) {
527 $fieldName = "custom_{$fieldID}";
528 if ($isMultiple[$tableName]) {
529 if ($formatMultiRecordField) {
530 $result["{$dao->id}"]["{$fieldID}"] = $dao->$fieldName;
531 }
532 else {
533 $result["{$fieldID}_{$dao->id}"] = $dao->$fieldName;
534 }
535 }
536 else {
537 $result[$fieldID] = $dao->$fieldName;
538 }
539 }
540 }
541 }
542 if (!empty($sortedResult)) {
543 $result['sortedResult'] = $sortedResult;
544 }
545 return $result;
546 }
547
548 /**
549 * Take in an array of entityID, custom_XXX => value
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 *
555 * @param array $params
556 *
557 * @throws Exception
558 * @return array
559 */
560 public static function setValues(&$params) {
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 }
565
566 if (!isset($params['entityID']) || !CRM_Utils_Type::validate($params['entityID'], 'Integer', FALSE)) {
567 throw new CRM_Core_Exception(ts('entity_id needs to be set and of type Integer'));
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
572 $fieldValues = [];
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) {
577 throw new CRM_Core_Exception(ts('field ID needs to be of type Integer for index %1',
578 [1 => $fieldID]
579 ));
580 }
581 if (!array_key_exists($fieldID, $fieldValues)) {
582 $fieldValues[$fieldID] = [];
583 }
584 $id = -1;
585 if ($customFieldInfo[1]) {
586 $id = (int ) $customFieldInfo[1];
587 }
588 $fieldValues[$fieldID][] = [
589 'value' => $v,
590 'id' => $id,
591 ];
592 }
593 }
594
595 $fieldIDList = implode(',', array_keys($fieldValues));
596
597 // format it so that we can just use create
598 $sql = "
599 SELECT cg.table_name as table_name ,
600 cg.id as cg_id ,
601 cg.is_multiple as is_multiple,
602 cg.extends as extends,
603 cf.column_name as column_name,
604 cf.id as cf_id ,
605 cf.html_type as html_type ,
606 cf.data_type as data_type ,
607 cf.serialize as serialize
608 FROM civicrm_custom_group cg,
609 civicrm_custom_field cf
610 WHERE cf.custom_group_id = cg.id
611 AND cf.id IN ( $fieldIDList )
612 ";
613
614 $dao = CRM_Core_DAO::executeQuery($sql);
615 $cvParams = [];
616
617 while ($dao->fetch()) {
618 $dataType = $dao->data_type == 'Date' ? 'Timestamp' : $dao->data_type;
619 foreach ($fieldValues[$dao->cf_id] as $fieldValue) {
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 }
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':
642 $fieldValue['value'] = (int) 0;
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) {
648 throw new CRM_Core_Exception(ts('value: %1 is not of the right field data type: %2',
649 [
650 1 => $fieldValue['value'],
651 2 => $dao->data_type,
652 ]
653 ));
654 }
655
656 $cvParam = [
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,
665 'serialize' => $dao->serialize,
666 'extends' => $dao->extends,
667 ];
668
669 if (!empty($params['id'])) {
670 $cvParam['id'] = $params['id'];
671 }
672
673 if ($cvParam['type'] == 'File') {
674 $cvParam['file_id'] = $fieldValue['value'];
675 }
676
677 if (!array_key_exists($dao->table_name, $cvParams)) {
678 $cvParams[$dao->table_name] = [];
679 }
680
681 if (!array_key_exists($fieldValue['id'], $cvParams[$dao->table_name])) {
682 $cvParams[$dao->table_name][$fieldValue['id']] = [];
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);
694 return ['is_error' => 0, 'result' => 1];
695 }
696
697 throw new CRM_Core_Exception(ts('Unknown error'));
698 }
699
700 /**
701 * Take in an array of entityID, custom_ID
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 *
707 * Entity Type will be inferred by the custom fields you request
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 *
713 * @param array $params
714 *
715 * @throws Exception
716 * @return array
717 */
718 public static function getValues($params) {
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
732 $fieldIDs = [];
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',
739 [1 => $idx]
740 ));
741 }
742 $fieldIDs[] = (int) $idx;
743 }
744 }
745
746 $default = array_merge(['Contact'], CRM_Contact_BAO_ContactType::basicTypes(TRUE));
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
775 $result = [
776 'is_error' => 1,
777 'error_message' => 'No values found for the specified entity ID and custom field(s).',
778 ];
779 return $result;
780 }
781 else {
782 $result = [
783 'is_error' => 0,
784 'entityID' => $params['entityID'],
785 ];
786 foreach ($values as $id => $value) {
787 $result["custom_{$id}"] = $value;
788 }
789 return $result;
790 }
791 }
792
793 }