Skip to content

Commit

Permalink
Add DedupeRule, DedupeRuleGroup and DedupeException APIv4 entities
Browse files Browse the repository at this point in the history
  • Loading branch information
monishdeb committed Jun 2, 2021
1 parent edfe1ce commit 12d0326
Show file tree
Hide file tree
Showing 20 changed files with 1,713 additions and 1,565 deletions.
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

0 comments on commit 12d0326

Please sign in to comment.