}
// get previous stats
- $previousStats = CRM_Core_BAO_PrevNextCache::retrieve("{$cacheKeyString}_stats");
+ $previousStats = CRM_Dedupe_Merger::getMergeStats($cacheKeyString);
if (!empty($previousStats)) {
- if ($previousStats[0]['merged']) {
- $merged = $merged + $previousStats[0]['merged'];
+ if ($previousStats['merged']) {
+ $merged = $merged + $previousStats['merged'];
}
- if ($previousStats[0]['skipped']) {
- $skipped = $skipped + $previousStats[0]['skipped'];
+ if ($previousStats['skipped']) {
+ $skipped = $skipped + $previousStats['skipped'];
}
}
*
* @return array
* Array of how many were merged and how many were skipped.
+ *
+ * @throws \CiviCRM_API3_Exception
*/
public static function getMergeStats($cacheKeyString) {
- $stats = CRM_Core_BAO_PrevNextCache::retrieve("{$cacheKeyString}_stats");
+ $stats = civicrm_api3('Dedupe', 'get', ['cachekey' => "{$cacheKeyString}_stats", 'sequential' => 1])['values'];
if (!empty($stats)) {
- $stats = $stats[0];
+ return $stats[0]['data'];
}
- return $stats;
+ return [];
}
/**
// return error
return FALSE;
}
- // Generate var $migrationInfo. The variable structure is exactly same as
- // $formValues submitted during a UI merge for a pair of contacts.
- $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($mainId, $otherId, $checkPermissions);
- // add additional details that we might need to resolve conflicts
- $rowsElementsAndInfo['migration_info']['main_details'] = &$rowsElementsAndInfo['main_details'];
- $rowsElementsAndInfo['migration_info']['other_details'] = &$rowsElementsAndInfo['other_details'];
- $rowsElementsAndInfo['migration_info']['rows'] = &$rowsElementsAndInfo['rows'];
-
- self::dedupePair($rowsElementsAndInfo['migration_info'], $resultStats, $deletedContacts, $mode, $checkPermissions, $mainId, $otherId, $cacheKeyString);
+
+ self::dedupePair($resultStats, $deletedContacts, $mode, $checkPermissions, $mainId, $otherId, $cacheKeyString);
}
if ($cacheKeyString && !$redirectForPerformance) {
* An empty array to be filed with conflict information.
*
* @return bool
+ *
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
+ * @throws \API_Exception
*/
public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe', &$conflicts = []) {
$conflicts = self::getConflicts($migrationInfo, $mainId, $otherId, $mode);
if (!empty($conflicts)) {
- foreach ($conflicts as $key => $val) {
- if ($val === NULL and $mode == 'safe') {
- // un-resolved conflicts still present. Lets skip this merge after saving the conflict / reason.
- return TRUE;
- }
- else {
- // copy over the resolved values
- $migrationInfo[$key] = $val;
- }
- }
// if there are conflicts and mode is aggressive, allow hooks to decide if to skip merges
return (bool) $migrationInfo['skip_merge'];
}
* @return bool
*/
public static function locationIsSame($mainAddress, $comparisonAddress) {
- $keysToIgnore = [
- 'id',
- 'is_primary',
- 'is_billing',
- 'manual_geo_code',
- 'contact_id',
- 'reset_date',
- 'hold_date',
- ];
+ $keysToIgnore = self::ignoredFields();
foreach ($comparisonAddress as $field => $value) {
if (in_array($field, $keysToIgnore)) {
continue;
* @param int $otherId
* Duplicate contact which would be deleted after merge operation.
*
- * @param $migrationInfo
+ * @param array $migrationInfo
*
* @param bool $checkPermissions
* Respect logged in user permissions.
*
* @return bool
+ * @throws \CiviCRM_API3_Exception
*/
public static function moveAllBelongings($mainId, $otherId, $migrationInfo, $checkPermissions = TRUE) {
if (empty($migrationInfo)) {
// **** Do contact related migrations
$customTablesToCopyValues = self::getAffectedCustomTables($submittedCustomFields);
+ // @todo - move all custom field processing to the move class & eventually have an
+ // overridable DAO class for it.
+ $customFieldBAO = new CRM_Core_BAO_CustomField();
+ $customFieldBAO->move($otherId, $mainId, $submittedCustomFields);
CRM_Dedupe_Merger::moveContactBelongings($mainId, $otherId, $moveTables, $tableOperations, $customTablesToCopyValues);
unset($moveTables, $tableOperations);
if (!isset($submitted)) {
$submitted = [];
}
- $customFiles = [];
foreach ($submitted as $key => $value) {
- list($cFields, $customFiles, $submitted) = self::processCustomFields($mainId, $key, $cFields, $customFiles, $submitted, $value);
+ list($cFields, $submitted) = self::processCustomFields($mainId, $key, $cFields, $submitted, $value);
}
- self::processCustomFieldFiles($mainId, $otherId, $customFiles);
-
// move view only custom fields CRM-5362
$viewOnlyCustomFields = [];
foreach ($submitted as $key => $value) {
CRM_Core_BAO_CustomValueTable::setValues($viewOnlyCustomFields);
}
+ // dev/core#996 Ensure that the earliest created date is stored against the kept contact id
+ $mainCreatedDate = civicrm_api3('Contact', 'getsingle', [
+ 'id' => $mainId,
+ 'return' => ['created_date'],
+ ])['created_date'];
+ $otherCreatedDate = civicrm_api3('Contact', 'getsingle', [
+ 'id' => $otherId,
+ 'return' => ['created_date'],
+ ])['created_date'];
+ if ($otherCreatedDate < $mainCreatedDate) {
+ CRM_Core_DAO::executeQuery("UPDATE civicrm_contact SET created_date = %1 WHERE id = %2", [
+ 1 => [$otherCreatedDate, 'String'],
+ 2 => [$mainId, 'Positive'],
+ ]);
+ }
+
if (!$checkPermissions || (CRM_Core_Permission::check('merge duplicate contacts') &&
CRM_Core_Permission::check('delete contacts'))
) {
if (!isset($submitted['suffix_id']) && !empty($migrationInfo['main_details']['suffix_id'])) {
$submitted['suffix_id'] = $migrationInfo['main_details']['suffix_id'];
}
-
- CRM_Contact_BAO_Contact::createProfileContact($submitted, CRM_Core_DAO::$_nullArray, $mainId);
+ $null = [];
+ CRM_Contact_BAO_Contact::createProfileContact($submitted, $null, $mainId);
}
$transaction->commit();
CRM_Utils_Hook::post('merge', 'Contact', $mainId);
public static function getDuplicatePairs($rule_group_id, $group_id, $reloadCacheIfEmpty, $batchLimit, $isSelected, $includeConflicts = TRUE, $criteria = [], $checkPermissions = TRUE, $searchLimit = 0) {
$dupePairs = self::getCachedDuplicateMatches($rule_group_id, $group_id, $batchLimit, $isSelected, $includeConflicts, $criteria, $checkPermissions);
if (empty($dupePairs) && $reloadCacheIfEmpty) {
- $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($rule_group_id, $group_id, $criteria, $checkPermissions);
// If we haven't found any dupes, probably cache is empty.
// Try filling cache and give another try. We don't need to specify include conflicts here are there will not be any
// until we have done some processing.
- CRM_Core_BAO_PrevNextCache::refillCache($rule_group_id, $group_id, $cacheKeyString, $criteria, $checkPermissions, $searchLimit);
+ CRM_Core_BAO_PrevNextCache::refillCache($rule_group_id, $group_id, $criteria, $checkPermissions, $searchLimit);
return self::getCachedDuplicateMatches($rule_group_id, $group_id, $batchLimit, $isSelected, FALSE, $criteria, $checkPermissions);
}
return $dupePairs;
/**
* Dedupe a pair of contacts.
*
- * @param array $migrationInfo
* @param array $resultStats
* @param array $deletedContacts
* @param string $mode
* @param int $mainId
* @param int $otherId
* @param string $cacheKeyString
+ *
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
+ * @throws \API_Exception
*/
- protected static function dedupePair(&$migrationInfo, &$resultStats, &$deletedContacts, $mode, $checkPermissions, $mainId, $otherId, $cacheKeyString) {
+ protected static function dedupePair(&$resultStats, &$deletedContacts, $mode, $checkPermissions, $mainId, $otherId, $cacheKeyString) {
- // go ahead with merge if there is no conflict
+ $migrationInfo = [];
$conflicts = [];
if (!CRM_Dedupe_Merger::skipMerge($mainId, $otherId, $migrationInfo, $mode, $conflicts)) {
CRM_Dedupe_Merger::moveAllBelongings($mainId, $otherId, $migrationInfo, $checkPermissions);
// store any conflicts
if (!empty($conflicts)) {
- foreach ($conflicts as $key => $dnc) {
- $conflicts[$key] = "{$migrationInfo['rows'][$key]['title']}: '{$migrationInfo['rows'][$key]['main']}' vs. '{$migrationInfo['rows'][$key]['other']}'";
- }
- CRM_Core_BAO_PrevNextCache::markConflict($mainId, $otherId, $cacheKeyString, $conflicts);
+ CRM_Core_BAO_PrevNextCache::markConflict($mainId, $otherId, $cacheKeyString, $conflicts, $mode);
}
else {
CRM_Core_BAO_PrevNextCache::deletePair($mainId, $otherId, $cacheKeyString);
/**
* Honestly - what DOES this do - hopefully some refactoring will reveal it's purpose.
*
+ * Update this function formats fields in preparation for them to be submitted to the
+ * 'ProfileContactCreate action. This is a lot of code to do this & for
+ * - for some fields it fails - e.g Country - per testMergeCustomFields.
+ *
+ * Goal is to move all custom field handling into 'move' functions on the various BAO
+ * with an underlying DAO function. For custom fields it has been started on the BAO.
+ *
* @param $mainId
* @param $key
* @param $cFields
- * @param $customFiles
* @param $submitted
* @param $value
*
* @return array
+ * @throws \Exception
*/
- protected static function processCustomFields($mainId, $key, $cFields, $customFiles, $submitted, $value) {
+ protected static function processCustomFields($mainId, $key, $cFields, $submitted, $value) {
if (substr($key, 0, 7) == 'custom_') {
$fid = (int) substr($key, 7);
if (empty($cFields[$fid])) {
- return [$cFields, $customFiles, $submitted];
+ return [$cFields, $submitted];
}
$htmlType = $cFields[$fid]['attributes']['html_type'];
switch ($htmlType) {
case 'File':
- $customFiles[] = $fid;
+ // Handled in CustomField->move(). Tested in testMergeCustomFields.
unset($submitted["custom_$fid"]);
break;
case 'Select Country':
+ // @todo Test in testMergeCustomFields disabled as this does not work, Handle in CustomField->move().
case 'Select State/Province':
$submitted[$key] = CRM_Core_BAO_CustomField::displayValue($value, $fid);
break;
break;
}
}
- return [$cFields, $customFiles, $submitted];
+ return [$cFields, $submitted];
}
/**
* - Does a force merge otherwise (aggressive mode).
*
* @return array
+ *
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
*/
public static function getConflicts(&$migrationInfo, $mainId, $otherId, $mode) {
$conflicts = [];
+ // Generate var $migrationInfo. The variable structure is exactly same as
+ // $formValues submitted during a UI merge for a pair of contacts.
+ $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($mainId, $otherId, FALSE);
+ // add additional details that we might need to resolve conflicts
+ $migrationInfo = $rowsElementsAndInfo['migration_info'];
+ $migrationInfo['main_details'] = &$rowsElementsAndInfo['main_details'];
+ $migrationInfo['other_details'] = &$rowsElementsAndInfo['other_details'];
+ $migrationInfo['rows'] = &$rowsElementsAndInfo['rows'];
+ // go ahead with merge if there is no conflict
$originalMigrationInfo = $migrationInfo;
foreach ($migrationInfo as $key => $val) {
if ($val === "null") {
$conflicts = $migrationData['fields_in_conflict'];
// allow hook to override / manipulate migrationInfo as well
$migrationInfo = $migrationData['migration_info'];
- $migrationInfo['skip_merge'] = CRM_Utils_Array::value('skip_merge', $migrationData);
- return $conflicts;
+ foreach ($conflicts as $key => $val) {
+ if ($val !== NULL || $mode !== 'safe') {
+ // copy over the resolved values
+ $migrationInfo[$key] = $val;
+ unset($conflicts[$key]);
+ }
+ }
+ $migrationInfo['skip_merge'] = $migrationData['skip_merge'] ?? !empty($conflicts);
+ return self::formatConflictArray($conflicts, $migrationInfo['rows'], $migrationInfo['main_details']['location_blocks'], $migrationInfo['other_details']['location_blocks'], $mainId, $otherId);
}
/**
- * Do file custom fields related migrations.
- * FIXME: move this someplace else (one of the BAOs) after discussing
- * where to, and whether CRM_Core_BAO_File::deleteFileReferences() shouldn't actually,
- * like, delete a file...
- *
- * Note outstanding bug https://lab.civicrm.org/dev/core/issues/723
- * relates to this code....
+ * @param array $conflicts
+ * @param array $migrationInfo
+ * @param $toKeepContactLocationBlocks
+ * @param $toRemoveContactLocationBlocks
+ * @param $toKeepID
+ * @param $toRemoveID
*
- * @param $mainId
- * @param $otherId
- * @param $customFiles
+ * @return mixed
+ * @throws \CRM_Core_Exception
*/
- protected static function processCustomFieldFiles($mainId, $otherId, $customFiles) {
- foreach ($customFiles as $customId) {
- list($tableName, $columnName, $groupID) = CRM_Core_BAO_CustomField::getTableColumnGroup($customId);
-
- // get the contact_id -> file_id mapping
- $fileIds = [];
- $sql = "SELECT entity_id, {$columnName} AS file_id FROM {$tableName} WHERE entity_id IN ({$mainId}, {$otherId})";
- $dao = CRM_Core_DAO::executeQuery($sql);
- while ($dao->fetch()) {
- // @todo - this is actually broken - fix & or remove - see testMergeCustomFields
- $fileIds[$dao->entity_id] = $dao->file_id;
- if ($dao->entity_id == $mainId) {
- CRM_Core_BAO_File::deleteFileReferences($fileIds[$mainId], $mainId, $customId);
+ protected static function formatConflictArray($conflicts, $migrationInfo, $toKeepContactLocationBlocks, $toRemoveContactLocationBlocks, $toKeepID, $toRemoveID) {
+ $return = [];
+ foreach (array_keys($conflicts) as $index) {
+ if (substr($index, 0, 14) === 'move_location_') {
+ $parts = explode('_', $index);
+ $entity = $parts[2];
+ $blockIndex = $parts[3];
+ $locationTypeID = $toKeepContactLocationBlocks[$entity][$blockIndex]['location_type_id'];
+ $entityConflicts = [
+ 'location_type_id' => $locationTypeID,
+ 'title' => $migrationInfo[$index]['title'],
+ ];
+ foreach ($toKeepContactLocationBlocks[$entity][$blockIndex] as $fieldName => $fieldValue) {
+ if (in_array($fieldName, self::ignoredFields())) {
+ continue;
+ }
+ $toRemoveValue = CRM_Utils_Array::value($fieldName, $toRemoveContactLocationBlocks[$entity][$blockIndex]);
+ if ($fieldValue !== $toRemoveValue) {
+ $entityConflicts[$fieldName] = [
+ $toKeepID => $fieldValue,
+ $toRemoveID => $toRemoveValue,
+ ];
+ }
}
- }
-
- // move the other contact's file to main contact
- //NYSS need to INSERT or UPDATE depending on whether main contact has an existing record
- if (CRM_Core_DAO::singleValueQuery("SELECT id FROM {$tableName} WHERE entity_id = {$mainId}")) {
- $sql = "UPDATE {$tableName} SET {$columnName} = {$fileIds[$otherId]} WHERE entity_id = {$mainId}";
- }
- else {
- $sql = "INSERT INTO {$tableName} ( entity_id, {$columnName} ) VALUES ( {$mainId}, {$fileIds[$otherId]} )";
- }
- CRM_Core_DAO::executeQuery($sql);
-
- if (CRM_Core_DAO::singleValueQuery("
- SELECT id
- FROM civicrm_entity_file
- WHERE entity_table = '{$tableName}' AND file_id = {$fileIds[$otherId]}")
- ) {
- $sql = "
- UPDATE civicrm_entity_file
- SET entity_id = {$mainId}
- WHERE entity_table = '{$tableName}' AND file_id = {$fileIds[$otherId]}";
+ $return[$entity][] = $entityConflicts;
+ }
+ elseif (substr($index, 0, 5) === 'move_') {
+ $contactFieldsToCompare[] = str_replace('move_', '', $index);
+ $return['contact'][str_replace('move_', '', $index)] = [
+ 'title' => $migrationInfo[$index]['title'],
+ $toKeepID => $migrationInfo[$index]['main'],
+ $toRemoveID => $migrationInfo[$index]['other'],
+ ];
}
else {
- $sql = "
- INSERT INTO civicrm_entity_file ( entity_table, entity_id, file_id )
- VALUES ( '{$tableName}', {$mainId}, {$fileIds[$otherId]} )";
+ // Can't think of why this would be the case but perhaps it's ensuring it isn't as we
+ // refactor this.
+ throw new CRM_Core_Exception(ts('Unknown parameter') . $index);
}
- CRM_Core_DAO::executeQuery($sql);
}
+ return $return;
}
/**
- * @param $rule_group_id
- * @param $group_id
- * @param $batchLimit
- * @param $isSelected
- * @param $includeConflicts
- * @param $criteria
- * @param $checkPermissions
+ * Get any duplicate merge pairs that have been previously cached.
+ *
+ * @param int $rule_group_id
+ * @param int $group_id
+ * @param int $batchLimit
+ * @param bool $isSelected
+ * @param bool $includeConflicts
+ * @param array $criteria
+ * @param int $checkPermissions
*
* @return array
*/
);
}
+ /**
+ * @return array
+ */
+ protected static function ignoredFields(): array {
+ $keysToIgnore = [
+ 'id',
+ 'is_primary',
+ 'is_billing',
+ 'manual_geo_code',
+ 'contact_id',
+ 'reset_date',
+ 'hold_date',
+ ];
+ return $keysToIgnore;
+ }
+
}