3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Core_BAO_CustomValueTable
{
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.
29 public static function create($customParams, $parentOperation = NULL) {
30 if (empty($customParams) ||
31 !is_array($customParams)
36 $paramFieldsExtendContactForEntities = [];
37 $VS = CRM_Core_DAO
::VALUE_SEPARATOR
;
39 foreach ($customParams as $tableName => $tables) {
40 foreach ($tables as $fields) {
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'];
58 $sqlOP = "INSERT INTO $tableName ";
63 CRM_Utils_Hook
::customPre(
65 (int) $firstField['custom_group_id'],
70 foreach ($fields as $field) {
71 // fix the value before we store it
72 $value = $field['value'];
73 $type = $field['type'];
77 if (is_array($value)) {
78 $value = CRM_Core_DAO
::VALUE_SEPARATOR
. implode(CRM_Core_DAO
::VALUE_SEPARATOR
, $value) . CRM_Core_DAO
::VALUE_SEPARATOR
;
81 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO
::VALUE_SEPARATOR
)) {
82 //fix for multi select state, CRM-3437
83 $mulValues = explode(',', $value);
85 foreach ($mulValues as $key => $stateVal) {
87 $states['state_province'] = trim($stateVal);
89 CRM_Utils_Array
::lookupValue($states, 'state_province',
90 CRM_Core_PseudoConstant
::stateProvince(), TRUE
92 if (empty($states['state_province_id'])) {
93 CRM_Utils_Array
::lookupValue($states, 'state_province',
94 CRM_Core_PseudoConstant
::stateProvinceAbbreviation(), TRUE
97 $validStates[] = $states['state_province_id'] ??
NULL;
99 $value = implode(CRM_Core_DAO
::VALUE_SEPARATOR
,
106 // using type of timestamp allows us to sneak in a null into db
107 // gross but effective hack
118 if (is_array($value)) {
119 $value = CRM_Core_DAO
::VALUE_SEPARATOR
. implode(CRM_Core_DAO
::VALUE_SEPARATOR
, $value) . CRM_Core_DAO
::VALUE_SEPARATOR
;
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) {
128 $countries['country'] = trim($countryVal);
129 CRM_Utils_Array
::lookupValue($countries, 'country',
130 CRM_Core_PseudoConstant
::country(), TRUE
132 if (empty($countries['country_id'])) {
133 CRM_Utils_Array
::lookupValue($countries, 'country',
134 CRM_Core_PseudoConstant
::countryIsoCode(), TRUE
137 $validCountries[] = $countries['country_id'] ??
NULL;
139 $value = implode(CRM_Core_DAO
::VALUE_SEPARATOR
,
146 // using type of timestamp allows us to sneak in a null into db
147 // gross but effective hack
157 if (!$field['file_id']) {
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);
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'];
176 $value = CRM_Utils_Date
::isoToMysql($value);
180 if (is_numeric($value)) {
188 case 'ContactReference':
189 if ($value == NULL ||
$value === '' ||
$value === $VS . $VS) {
193 elseif (strpos($value, $VS) !== FALSE) {
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');
206 case 'RichTextEditor':
212 $value = CRM_Utils_String
::strtoboolstr($value);
213 if ($value === FALSE) {
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;
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
232 $params[$count] = [$value, ($value && $field['serialize']) ?
'String' : $type];
236 $fieldExtends = $field['extends'] ??
NULL;
238 CRM_Utils_Array
::value('entity_table', $field) === 'civicrm_contact'
239 ||
$fieldExtends === 'Contact'
240 ||
$fieldExtends === 'Individual'
241 ||
$fieldExtends === 'Organization'
242 ||
$fieldExtends === 'Household'
244 $paramFieldsExtendContactForEntities[$entityID]['custom_' . CRM_Utils_Array
::value('custom_field_id', $field)] = $field['custom_field_id'] ??
NULL;
250 foreach ($set as $n => $v) {
251 $setClause[] = "`$n` = $v";
253 $setClause = implode(',', $setClause);
255 // do this only for insert
256 $set['entity_id'] = "%{$count}";
257 $params[$count] = [$entityID, 'Integer'];
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";
269 $query = "$sqlOP SET $setClause $where";
271 CRM_Core_DAO
::executeQuery($query, $params);
273 CRM_Utils_Hook
::custom($hookOP,
274 (int) $firstField['custom_group_id'],
282 if (!empty($paramFieldsExtendContactForEntities)) {
283 CRM_Contact_BAO_Contact
::updateGreetingsOnTokenFieldChange($paramFieldsExtendContactForEntities, ['contact_id' => $entityID]);
288 * Given a field return the mysql data type associated with it.
290 * @param string $type
291 * @param int $maxLength
294 * the mysql data store placeholder
296 public static function fieldToSQLType($type, $maxLength = 255) {
297 if (!isset($maxLength) ||
298 !is_numeric($maxLength) ||
307 return "varchar($maxLength)";
315 // the below three are FK's, and have constraints added to them
317 case 'ContactReference':
318 case 'StateProvince':
321 return 'int unsigned';
327 return 'decimal(20,2)';
330 case 'RichTextEditor':
337 throw new CRM_Core_Exception('Invalid Field Type');
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.
351 public static function store($params, $entityTable, $entityID, $parentOperation = NULL) {
353 foreach ($params as $fieldID => $param) {
354 foreach ($param as $index => $customValue) {
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'],
371 // Fix Date type to be timestamp, since that is how we store in db.
372 if ($cvParam['type'] == 'Date') {
373 $cvParam['type'] = 'Timestamp';
376 if (!empty($customValue['id'])) {
377 $cvParam['id'] = $customValue['id'];
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;
386 if (!array_key_exists($customValue['table_name'], $cvParams)) {
387 $cvParams[$customValue['table_name']] = [];
390 if (!array_key_exists($index, $cvParams[$customValue['table_name']])) {
391 $cvParams[$customValue['table_name']][$index] = [];
394 $cvParams[$customValue['table_name']][$index][] = $cvParam;
397 if (!empty($cvParams)) {
398 self
::create($cvParams, $parentOperation);
403 * Post process function.
405 * @param array $params
406 * @param $entityTable
407 * @param int $entityID
408 * @param $customFieldExtends
409 * @param $parentOperation
411 public static function postProcess(&$params, $entityTable, $entityID, $customFieldExtends, $parentOperation = NULL) {
412 $customData = CRM_Core_BAO_CustomField
::postProcess($params,
417 if (!empty($customData)) {
418 self
::store($customData, $entityTable, $entityID, $parentOperation);
423 * Return an array of all custom values associated with an entity.
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
434 * @param array $fieldIDs
435 * Optional list of fieldIDs that we want to retrieve. If this.
436 * is set the entityType is ignored
438 * @param bool $formatMultiRecordField
439 * @param array $DTparams - CRM-17810 dataTable params for the multiValued custom fields.
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
447 public static function getEntityValues($entityID, $entityType = NULL, $fieldIDs = NULL, $formatMultiRecordField = FALSE, $DTparams = NULL) {
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');
457 $cond[] = "cg.extends IN ( '$entityType' )";
462 $fieldIDList = implode(',', $fieldIDs);
463 $cond[] = "cf.id IN ( $fieldIDList )";
466 $contactTypes = array_merge(['Contact'], CRM_Contact_BAO_ContactType
::basicTypes(TRUE));
467 $cond[] = "cg.extends IN ( '" . implode("', '", $contactTypes) . "' )";
469 $cond = implode(' AND ', $cond);
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');
475 if (!empty($DTparams['sort'])) {
476 $orderBy = ' ORDER BY ' . CRM_Utils_Type
::escape($DTparams['sort'], 'String');
479 // First find all the fields that extend this type of entity.
481 SELECT cg.table_name,
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
494 $dao = CRM_Core_DAO
::executeQuery($query);
496 $select = $fields = $isMultiple = [];
498 while ($dao->fetch()) {
499 if (!array_key_exists($dao->table_name
, $select)) {
500 $fields[$dao->table_name
] = [];
501 $select[$dao->table_name
] = [];
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
;
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}");
514 while ($query->fetch()) {
515 $sortedResult["{$query->id}"] = $count;
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()');
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;
533 $result["{$fieldID}_{$dao->id}"] = $dao->$fieldName;
537 $result[$fieldID] = $dao->$fieldName;
542 if (!empty($sortedResult)) {
543 $result['sortedResult'] = $sortedResult;
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
555 * @param array $params
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'];
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'));
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
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',
581 if (!array_key_exists($fieldID, $fieldValues)) {
582 $fieldValues[$fieldID] = [];
585 if ($customFieldInfo[1]) {
586 $id = (int ) $customFieldInfo[1];
588 $fieldValues[$fieldID][] = [
595 $fieldIDList = implode(',', array_keys($fieldValues));
597 // format it so that we can just use create
599 SELECT cg.table_name as table_name ,
601 cg.is_multiple as is_multiple,
602 cg.extends as extends,
603 cf.column_name as column_name,
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 )
614 $dao = CRM_Core_DAO
::executeQuery($sql);
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']);
624 // Format null values correctly
625 if ($fieldValue['value'] === NULL ||
$fieldValue['value'] === '') {
631 $fieldValue['value'] = '';
635 $fieldValue['value'] = NULL;
638 case 'StateProvince':
642 $fieldValue['value'] = (int) 0;
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',
650 1 => $fieldValue['value'],
651 2 => $dao->data_type
,
657 'entity_id' => $params['entityID'],
658 'value' => $fieldValue['value'],
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,
669 if (!empty($params['id'])) {
670 $cvParam['id'] = $params['id'];
673 if ($cvParam['type'] == 'File') {
674 $cvParam['file_id'] = $fieldValue['value'];
677 if (!array_key_exists($dao->table_name
, $cvParams)) {
678 $cvParams[$dao->table_name
] = [];
681 if (!array_key_exists($fieldValue['id'], $cvParams[$dao->table_name
])) {
682 $cvParams[$dao->table_name
][$fieldValue['id']] = [];
685 if ($fieldValue['id'] > 0) {
686 $cvParam['id'] = $fieldValue['id'];
688 $cvParams[$dao->table_name
][$fieldValue['id']][] = $cvParam;
692 if (!empty($cvParams)) {
693 self
::create($cvParams);
694 return ['is_error' => 0, 'result' => 1];
697 throw new CRM_Core_Exception(ts('Unknown error'));
701 * Take in an array of entityID, custom_ID
702 * and gets the value from the appropriate table.
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 );
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
713 * @param array $params
718 public static function getValues($params) {
719 if (empty($params)) {
722 if (!isset($params['entityID']) ||
723 CRM_Utils_Type
::escape($params['entityID'],
727 return CRM_Core_Error
::createAPIError(ts('entityID needs to be set and of type Integer'));
730 // first collect all the ids. The format is:
733 foreach ($params as $n => $v) {
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',
742 $fieldIDs[] = (int) $idx;
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)
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)) {
762 return CRM_Core_Error
::createAPIError(ts('Invalid entity type') . ': "' . $type . '"');
767 $values = self
::getEntityValues($params['entityID'],
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
777 'error_message' => 'No values found for the specified entity ID and custom field(s).',
784 'entityID' => $params['entityID'],
786 foreach ($values as $id => $value) {
787 $result["custom_{$id}"] = $value;