Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DedupeRule, DedupeRuleGroup and DedupeException API4 entity #20466

Merged
merged 2 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CRM/Core/DAO/AllCoreTables.data.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,19 +207,19 @@
'class' => 'CRM_Event_Cart_DAO_Cart',
'table' => 'civicrm_event_carts',
],
'CRM_Dedupe_DAO_RuleGroup' => [
'name' => 'RuleGroup',
'class' => 'CRM_Dedupe_DAO_RuleGroup',
'CRM_Dedupe_DAO_DedupeRuleGroup' => [
'name' => 'DedupeRuleGroup',
'class' => 'CRM_Dedupe_DAO_DedupeRuleGroup',
'table' => 'civicrm_dedupe_rule_group',
],
'CRM_Dedupe_DAO_Rule' => [
'name' => 'Rule',
'class' => 'CRM_Dedupe_DAO_Rule',
'CRM_Dedupe_DAO_DedupeRule' => [
'name' => 'DedupeRule',
'class' => 'CRM_Dedupe_DAO_DedupeRule',
'table' => 'civicrm_dedupe_rule',
],
'CRM_Dedupe_DAO_Exception' => [
'name' => 'Exception',
'class' => 'CRM_Dedupe_DAO_Exception',
'CRM_Dedupe_DAO_DedupeException' => [
'name' => 'DedupeException',
'class' => 'CRM_Dedupe_DAO_DedupeException',
'table' => 'civicrm_dedupe_exception',
],
'CRM_Case_DAO_CaseType' => [
Expand Down
56 changes: 56 additions & 0 deletions CRM/Dedupe/BAO/DedupeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/

/**
* Manages dedupe exceptions - ie pairs marked as non-duplicates.
*/
class CRM_Dedupe_BAO_DedupeException extends CRM_Dedupe_DAO_DedupeException {

/**
* Create a dedupe exception record.
*
* @param array $params
*
* @return \CRM_Dedupe_BAO_Exception
*/
public static function create($params) {
$hook = empty($params['id']) ? 'create' : 'edit';
CRM_Utils_Hook::pre($hook, 'Exception', CRM_Utils_Array::value('id', $params), $params);
$contact1 = $params['contact_id1'] ?? NULL;
$contact2 = $params['contact_id2'] ?? NULL;
$dao = new CRM_Dedupe_BAO_Exception();
$dao->copyValues($params);
if ($contact1 && $contact2) {
CRM_Core_DAO::singleValueQuery("
DELETE FROM civicrm_prevnext_cache
WHERE (entity_id1 = %1 AND entity_id2 = %2)
OR (entity_id1 = %2 AND entity_id2 = %2)",
[1 => [$contact1, 'Integer'], 2 => [$contact2, 'Integer']]
);
if ($contact2 < $contact1) {
// These are expected to be saved lowest first.
$dao->contact_id1 = $contact2;
$dao->contact_id2 = $contact1;
}
}
$dao->save();

CRM_Utils_Hook::post($hook, 'Exception', $dao->id, $dao);
return $dao;
}

}
252 changes: 252 additions & 0 deletions CRM/Dedupe/BAO/DedupeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/

/**
* The CiviCRM duplicate discovery engine is based on an
* algorithm designed by David Strauss <david@fourkitchens.com>.
*/
class CRM_Dedupe_BAO_DedupeRule extends CRM_Dedupe_DAO_DedupeRule {

/**
* Ids of the contacts to limit the SQL queries (whole-database queries otherwise)
* @var array
*/
public $contactIds = [];

/**
* Params to dedupe against (queries against the whole contact set otherwise)
* @var array
*/
public $params = [];

/**
* Return the SQL query for the given rule - either for finding matching
* pairs of contacts, or for matching against the $params variable (if set).
*
* @return string
* SQL query performing the search
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
public function sql() {
if ($this->params &&
(!array_key_exists($this->rule_table, $this->params) ||
!array_key_exists($this->rule_field, $this->params[$this->rule_table])
)
) {
// if params is present and doesn't have an entry for a field, don't construct the clause.
return NULL;
}

// we need to initialise WHERE, ON and USING here, as some table types
// extend them; $where is an array of required conditions, $on and
// $using are arrays of required field matchings (for substring and
// full matches, respectively)
$where = [];
$on = ["SUBSTR(t1.{$this->rule_field}, 1, {$this->rule_length}) = SUBSTR(t2.{$this->rule_field}, 1, {$this->rule_length})"];

$innerJoinClauses = [
"t1.{$this->rule_field} IS NOT NULL",
"t2.{$this->rule_field} IS NOT NULL",
"t1.{$this->rule_field} = t2.{$this->rule_field}",
];

if (in_array($this->getFieldType($this->rule_field), CRM_Utils_Type::getTextTypes(), TRUE)) {
$innerJoinClauses[] = "t1.{$this->rule_field} <> ''";
$innerJoinClauses[] = "t2.{$this->rule_field} <> ''";
}

switch ($this->rule_table) {
case 'civicrm_contact':
$id = 'id';
//we should restrict by contact type in the first step
$sql = "SELECT contact_type FROM civicrm_dedupe_rule_group WHERE id = {$this->dedupe_rule_group_id};";
$ct = CRM_Core_DAO::singleValueQuery($sql);
if ($this->params) {
$where[] = "t1.contact_type = '{$ct}'";
}
else {
$where[] = "t1.contact_type = '{$ct}'";
$where[] = "t2.contact_type = '{$ct}'";
}
break;

case 'civicrm_address':
case 'civicrm_email':
case 'civicrm_im':
case 'civicrm_openid':
case 'civicrm_phone':
$id = 'contact_id';
break;

case 'civicrm_note':
$id = 'entity_id';
if ($this->params) {
$where[] = "t1.entity_table = 'civicrm_contact'";
}
else {
$where[] = "t1.entity_table = 'civicrm_contact'";
$where[] = "t2.entity_table = 'civicrm_contact'";
}
break;

default:
// custom data tables
if (preg_match('/^civicrm_value_/', $this->rule_table) || preg_match('/^custom_value_/', $this->rule_table)) {
$id = 'entity_id';
}
else {
throw new CRM_Core_Exception("Unsupported rule_table for civicrm_dedupe_rule.id of {$this->id}");
}
break;
}

// build SELECT based on the field names containing contact ids
// if there are params provided, id1 should be 0
if ($this->params) {
$select = "t1.$id id1, {$this->rule_weight} weight";
$subSelect = 'id1, weight';
}
else {
$select = "t1.$id id1, t2.$id id2, {$this->rule_weight} weight";
$subSelect = 'id1, id2, weight';
}

// build FROM (and WHERE, if it's a parametrised search)
// based on whether the rule is about substrings or not
if ($this->params) {
$from = "{$this->rule_table} t1";
$str = 'NULL';
if (isset($this->params[$this->rule_table][$this->rule_field])) {
$str = trim(CRM_Utils_Type::escape($this->params[$this->rule_table][$this->rule_field], 'String'));
}
if ($this->rule_length) {
$where[] = "SUBSTR(t1.{$this->rule_field}, 1, {$this->rule_length}) = SUBSTR('$str', 1, {$this->rule_length})";
$where[] = "t1.{$this->rule_field} IS NOT NULL";
}
else {
$where[] = "t1.{$this->rule_field} = '$str'";
}
}
else {
if ($this->rule_length) {
$from = "{$this->rule_table} t1 JOIN {$this->rule_table} t2 ON (" . implode(' AND ', $on) . ")";
}
else {
$from = "{$this->rule_table} t1 INNER JOIN {$this->rule_table} t2 ON (" . implode(' AND ', $innerJoinClauses) . ")";
}
}

// finish building WHERE, also limit the results if requested
if (!$this->params) {
$where[] = "t1.$id < t2.$id";
}
$query = "SELECT $select FROM $from WHERE " . implode(' AND ', $where);
if ($this->contactIds) {
$cids = [];
foreach ($this->contactIds as $cid) {
$cids[] = CRM_Utils_Type::escape($cid, 'Integer');
}
if (count($cids) == 1) {
$query .= " AND (t1.$id = {$cids[0]}) UNION $query AND t2.$id = {$cids[0]}";
}
else {
$query .= " AND t1.$id IN (" . implode(',', $cids) . ")
UNION $query AND t2.$id IN (" . implode(',', $cids) . ")";
}
// The `weight` is ambiguous in the context of the union; put the whole
// thing in a subquery.
$query = "SELECT $subSelect FROM ($query) subunion";
}

return $query;
}

/**
* find fields related to a rule group.
*
* @param array $params contains the rule group property to identify rule group
*
* @return array
* rule fields array associated to rule group
*/
public static function dedupeRuleFields($params) {
$rgBao = new CRM_Dedupe_BAO_RuleGroup();
$rgBao->used = $params['used'];
$rgBao->contact_type = $params['contact_type'];
$rgBao->find(TRUE);

$ruleBao = new CRM_Dedupe_BAO_Rule();
$ruleBao->dedupe_rule_group_id = $rgBao->id;
$ruleBao->find();
$ruleFields = [];
while ($ruleBao->fetch()) {
$field_name = $ruleBao->rule_field;
if ($field_name == 'phone_numeric') {
$field_name = 'phone';
}
$ruleFields[] = $field_name;
}
return $ruleFields;
}

/**
* @param int $cid
* @param int $oid
*
* @return bool
*/
public static function validateContacts($cid, $oid) {
if (!$cid || !$oid) {
return NULL;
}
$exception = new CRM_Dedupe_DAO_Exception();
$exception->contact_id1 = $cid;
$exception->contact_id2 = $oid;
//make sure contact2 > contact1.
if ($cid > $oid) {
$exception->contact_id1 = $oid;
$exception->contact_id2 = $cid;
}

return !$exception->find(TRUE);
}

/**
* Get the specification for the given field.
*
* @param string $fieldName
*
* @return array
* @throws \CiviCRM_API3_Exception
*/
public function getFieldType($fieldName) {
$entity = CRM_Core_DAO_AllCoreTables::getBriefName(CRM_Core_DAO_AllCoreTables::getClassForTable($this->rule_table));
if (!$entity) {
// This means we have stored a custom field rather than an entity name in rule_table, figure out the entity.
$entity = civicrm_api3('CustomGroup', 'getvalue', ['table_name' => $this->rule_table, 'return' => 'extends']);
if (in_array($entity, ['Individual', 'Household', 'Organization'])) {
$entity = 'Contact';
}
$fieldName = 'custom_' . civicrm_api3('CustomField', 'getvalue', ['column_name' => $fieldName, 'return' => 'id']);
}
$fields = civicrm_api3($entity, 'getfields', ['action' => 'create'])['values'];
return $fields[$fieldName]['type'];
}

}
Loading