contacts-8.x-1.x-dev/src/Form/ContactsDuplicateForm.php
src/Form/ContactsDuplicateForm.php
<?php
namespace Drupal\contacts\Form;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form that displays duplicates for a single contact and allows exclusions.
*
* @package Drupal\contacts\Form
*/
class ContactsDuplicateForm extends FormBase {
/**
* Module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $db;
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current contact.
*
* @var \Drupal\user\UserInterface
*/
protected $contact;
/**
* ContactsDuplicateForm constructor.
*
* @param \Drupal\Core\Database\Connection $db
* Database.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* Module handler.
*/
public function __construct(Connection $db, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->db = $db;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('entity_type.manager'),
$container->get('module_handler')
);
}
/**
* Sets the contact used by this form.
*
* @param \Drupal\user\UserInterface $contact
* The contact.
*/
public function setContact(UserInterface $contact) {
$this->contact = $contact;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'contacts_duplicates';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$show_excluded = $form_state->getValue(['filters', 'excluded']);
$exclusions = $this->getExclusions();
$possible_duplicates = $this->findPossibleDuplicates($exclusions, $show_excluded);
$form['filters'] = [
'#tree' => TRUE,
'#type' => 'container',
];
$form['filters']['excluded'] = [
'#type' => 'select',
'#title' => $this->t('Show Excluded'),
'#options' => [$this->t('No'), $this->t('Yes')],
'#default_value' => $show_excluded,
'#description' => $this->t("Matches can be excluded from the results where staff don't think that this match is a real duplicate."),
];
$form['filters']['apply'] = [
'#type' => 'submit',
'#value' => $this->t('Apply'),
'#submit' => ['::applyFilters'],
];
$form['table'] = [
'#type' => 'table',
'#attributes' => ['class' => ['table', 'table-hover', 'table-striped']],
'#header' => [
$this->t('Name'),
$this->t('Email'),
$this->t('Address'),
$this->t('Has login?'),
$this->t('Excluded'),
'',
],
'#empty' => $this->t('No duplicates found'),
];
foreach ($possible_duplicates as $duplicate) {
$merge_link = '';
// Only generate the merge link if entity_merge is installed.
if ($this->moduleHandler->moduleExists('entity_merge')) {
$merge_link = Link::createFromRoute($this->t('Merge'), 'entity.entity_merge_request.verify', [
'entity_type' => 'user',
'primary_id' => $this->contact->id(),
'secondary_id' => $duplicate->id(),
])->toString();
}
$contact_link = Link::createFromRoute($duplicate->getDisplayName(), 'contacts.contact', [
'user' => $duplicate->id(),
]);
$form['table'][$duplicate->id()]['name'] = [
'#markup' => $contact_link->toString(),
];
$form['table'][$duplicate->id()]['email'] = [
'#plain_text' => $duplicate->getEmail(),
];
$form['table'][$duplicate->id()]['address'] = [
'#plain_text' => $this->formatAddress($duplicate->profile_crm_indiv->entity->crm_address),
];
$form['table'][$duplicate->id()]['has_login'] = [
'#markup' => $duplicate->getAccountName() ? $this->t('Yes') : $this->t('No'),
];
$form['table'][$duplicate->id()]['excluded'] = [
'#type' => 'checkbox',
// Blank label hack. Needs a label to be aligned properly.
'#title' => '',
'#default_value' => in_array($duplicate->id(), $exclusions),
];
$form['table'][$duplicate->id()]['merge'] = [
'#markup' => $merge_link,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save Exclusions'),
'#button_type' => 'primary',
'#submit' => ['::submitForm'],
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$exclusions = [];
foreach ($form_state->getValue('table') as $user_id => $row) {
if ($row['excluded']) {
$exclusions[] = $user_id;
}
}
$this->saveExclusions($exclusions);
$this->messenger()->addMessage($this->t('Exclusions saved.'));
}
/**
* Run when the apply filter button is clicked.
*
* @param array $form
* Form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*/
public function applyFilters(array &$form, FormStateInterface $form_state) {
$form_state->setRebuild();
}
/**
* Finds possible duplicates.
*
* @param array $exclusions
* Array of IDs excluded from deduplication.
* @param bool $show_excluded
* Whether the excluded users should be included in the results.
*
* @return \Drupal\user\Entity\User[]
* Array of users.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
private function findPossibleDuplicates(array $exclusions, $show_excluded) {
$profile = $this->contact->profile_crm_indiv->entity;
if (!$profile || !$profile->crm_name || !$profile->crm_address) {
return [];
}
$first_name = $profile->crm_name->given;
$last_name = $profile->crm_name->family;
$postcode = $profile->crm_address->postal_code;
// Find any with same last name, same post code and same first 2
// letters of first name. Don't use full first name, as this won't catch
// Sam vs Samantha etc on the duplicate.
if ($first_name && $last_name && $postcode) {
$query = $this->db->select('profile', 'p');
$query->join('profile__crm_name', 'n', 'n.entity_id = p.profile_id');
$query->join('profile__crm_address', 'a', 'a.entity_id = p.profile_id');
$query->leftJoin('contacts_duplicate_exclusions', 'ex', 'p.uid = ex.uid2 and ex.uid1 = :primary_id', [':primary_id' => $this->contact->id()]);
$query->condition('p.uid', $this->contact->id(), '<>');
$query->condition('p.type', 'crm_indiv');
$query->condition('n.crm_name_family', $last_name);
// Match on postcode, but replace spaces and trim in case there are
// formatting differences between the 2 records.
$query->where("TRIM(REPLACE(a.crm_address_postal_code, ' ', '')) = :postcode", [
':postcode' => trim(str_replace(' ', '', $postcode)),
]);
// Match on only first 2 chars of name. Handles differences eg Samantha
// vs Sam.
$query->where('LEFT(crm_name_given, 2) = :first', [
':first' => substr($first_name, 0, 2),
]);
if (!$show_excluded && count($exclusions)) {
$query->condition('p.uid', $exclusions, 'NOT IN');
}
$query->fields('p', ['uid']);
$user_ids = $query->execute()->fetchCol();
$users = $this->entityTypeManager->getStorage('user')
->loadMultiple($user_ids);
return $users;
}
return [];
}
/**
* Format address for display.
*
* @param \Drupal\Core\Field\FieldItemListInterface $address
* Address.
*
* @return string
* Formatted address.
*/
private function formatAddress(FieldItemListInterface $address) {
$to_format = array_filter([
$address->address_line1,
$address->address_line2,
$address->locality,
$address->administrative_area,
$address->postal_code,
]);
return implode(', ', $to_format);
}
/**
* Gets the IDs to exclude from the duplicate list.
*
* @return array
* Entity IDs to exclude.
*/
private function getExclusions() {
$q = $this->db->select('contacts_duplicate_exclusions', 'e');
$q->addField('e', 'uid2');
$q->condition('uid1', $this->contact->id());
return $q->execute()->fetchCol();
}
/**
* Saves the exclusions.
*
* @param array $exclusions
* Array of user IDs to exclude.
*/
private function saveExclusions(array $exclusions) {
$q = $this->db->delete('contacts_duplicate_exclusions');
$q->condition('uid1', $this->contact->id());
$q->execute();
foreach ($exclusions as $exclusion) {
$this->db->insert('contacts_duplicate_exclusions')
->fields(['uid1' => $this->contact->id(), 'uid2' => $exclusion])
->execute();
}
}
}
