Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
173
scripts/merge-duplicate-contacts.php
Normal file
173
scripts/merge-duplicate-contacts.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/app/Services/DB.php';
|
||||
require $root . '/app/Services/BackupService.php';
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
$db = DB::pdo();
|
||||
|
||||
function contactKey(string $value): string
|
||||
{
|
||||
$value = mb_strtolower(trim($value));
|
||||
$ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
if ($ascii !== false) {
|
||||
$value = $ascii;
|
||||
}
|
||||
$parts = preg_split('/[^a-z0-9]+/', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
sort($parts);
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
function filledScore(array $contact): int
|
||||
{
|
||||
$score = 0;
|
||||
foreach (['phone', 'email', 'address', 'postal_code', 'city', 'notes'] as $field) {
|
||||
if (trim((string) ($contact[$field] ?? '')) !== '') {
|
||||
$score++;
|
||||
}
|
||||
}
|
||||
return $score;
|
||||
}
|
||||
|
||||
$allContacts = $db
|
||||
->query("SELECT * FROM directory_contacts WHERE deleted_at IS NULL AND kind='person'")
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
$byKey = [];
|
||||
foreach ($allContacts as $contact) {
|
||||
$byKey[contactKey($contact['name'])][] = $contact;
|
||||
}
|
||||
$groups = array_filter($byKey, static fn(array $rows): bool => count($rows) > 1);
|
||||
$singletons = array_values(array_filter($byKey, static fn(array $rows): bool => count($rows) === 1));
|
||||
$used = [];
|
||||
for ($i = 0; $i < count($singletons); $i++) {
|
||||
for ($j = $i + 1; $j < count($singletons); $j++) {
|
||||
$a = $singletons[$i][0];
|
||||
$b = $singletons[$j][0];
|
||||
if (isset($used[$a['id']]) || isset($used[$b['id']])) {
|
||||
continue;
|
||||
}
|
||||
$ka = contactKey($a['name']);
|
||||
$kb = contactKey($b['name']);
|
||||
if (min(strlen($ka), strlen($kb)) >= 8 && levenshtein($ka, $kb) <= 1) {
|
||||
$groups['fuzzy:' . $a['id'] . ':' . $b['id']] = [$a, $b];
|
||||
$used[$a['id']] = $used[$b['id']] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$foreignKeys = [];
|
||||
foreach (
|
||||
$db
|
||||
->query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
->fetchAll(PDO::FETCH_COLUMN)
|
||||
as $table
|
||||
) {
|
||||
foreach (
|
||||
$db->query('PRAGMA foreign_key_list("' . str_replace('"', '""', $table) . '")')->fetchAll(PDO::FETCH_ASSOC)
|
||||
as $fk
|
||||
) {
|
||||
if ($fk['table'] === 'directory_contacts' && $table !== 'directory_contact_roles') {
|
||||
$foreignKeys[] = [$table, $fk['from']];
|
||||
}
|
||||
}
|
||||
}
|
||||
$summary = ['groups' => count($groups), 'contacts_merged' => 0, 'relations_moved' => 0];
|
||||
$details = [];
|
||||
$backup = null;
|
||||
if ($apply) {
|
||||
$backup = BackupService::create('avant-fusion-annuaire');
|
||||
}
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
foreach ($groups as $key => $contacts) {
|
||||
usort($contacts, static function (array $a, array $b): int {
|
||||
$byDetails = filledScore($b) <=> filledScore($a);
|
||||
return $byDetails !== 0 ? $byDetails : (int) $a['id'] <=> (int) $b['id'];
|
||||
});
|
||||
$canonical = array_shift($contacts);
|
||||
$canonicalId = (int) $canonical['id'];
|
||||
$mergedNames = [$canonical['name']];
|
||||
foreach ($contacts as $duplicate) {
|
||||
$duplicateId = (int) $duplicate['id'];
|
||||
$mergedNames[] = $duplicate['name'];
|
||||
foreach ($foreignKeys as [$table, $column]) {
|
||||
$countStmt = $db->prepare("SELECT COUNT(*) FROM \"$table\" WHERE \"$column\"=?");
|
||||
$countStmt->execute([$duplicateId]);
|
||||
$count = (int) $countStmt->fetchColumn();
|
||||
if ($count) {
|
||||
$db->prepare("UPDATE \"$table\" SET \"$column\"=? WHERE \"$column\"=?")->execute([
|
||||
$canonicalId,
|
||||
$duplicateId,
|
||||
]);
|
||||
$summary['relations_moved'] += $count;
|
||||
}
|
||||
}
|
||||
$roles = $db->prepare('SELECT role FROM directory_contact_roles WHERE contact_id=?');
|
||||
$roles->execute([$duplicateId]);
|
||||
foreach ($roles->fetchAll(PDO::FETCH_COLUMN) as $role) {
|
||||
$db->prepare('INSERT OR IGNORE INTO directory_contact_roles(contact_id,role) VALUES(?,?)')->execute([
|
||||
$canonicalId,
|
||||
$role,
|
||||
]);
|
||||
}
|
||||
$db->prepare('DELETE FROM directory_contact_roles WHERE contact_id=?')->execute([$duplicateId]);
|
||||
foreach (
|
||||
['phone', 'email', 'address', 'postal_code', 'city', 'country', 'organization_id', 'notes']
|
||||
as $field
|
||||
) {
|
||||
if (empty($canonical[$field]) && !empty($duplicate[$field])) {
|
||||
$canonical[$field] = $duplicate[$field];
|
||||
}
|
||||
}
|
||||
$db->prepare('DELETE FROM directory_contacts WHERE id=?')->execute([$duplicateId]);
|
||||
$summary['contacts_merged']++;
|
||||
}
|
||||
$db->prepare(
|
||||
'UPDATE directory_contacts SET phone=?,email=?,address=?,postal_code=?,city=?,country=?,organization_id=?,notes=?,updated_at=datetime(\'now\') WHERE id=?',
|
||||
)->execute([
|
||||
$canonical['phone'] ?: null,
|
||||
$canonical['email'] ?: null,
|
||||
$canonical['address'] ?: null,
|
||||
$canonical['postal_code'] ?: null,
|
||||
$canonical['city'] ?: null,
|
||||
$canonical['country'] ?: 'France',
|
||||
$canonical['organization_id'] ?: null,
|
||||
$canonical['notes'] ?: null,
|
||||
$canonicalId,
|
||||
]);
|
||||
foreach (array_unique($mergedNames) as $oldName) {
|
||||
$db->prepare(
|
||||
'UPDATE adoptions SET adopter_name=? WHERE adopter_contact_id=? AND lower(trim(adopter_name))=lower(trim(?))',
|
||||
)->execute([$canonical['name'], $canonicalId, $oldName]);
|
||||
$db->prepare(
|
||||
'UPDATE animal_movements SET contact_name=? WHERE lower(trim(contact_name))=lower(trim(?))',
|
||||
)->execute([$canonical['name'], $oldName]);
|
||||
}
|
||||
$details[] = ['kept_id' => $canonicalId, 'kept_name' => $canonical['name'], 'merged_names' => $mergedNames];
|
||||
}
|
||||
$fk = $db->query('PRAGMA foreign_key_check')->fetchAll();
|
||||
if ($fk) {
|
||||
throw new RuntimeException('Violation de clé étrangère après fusion.');
|
||||
}
|
||||
if ($apply) {
|
||||
$db->commit();
|
||||
} else {
|
||||
$db->rollBack();
|
||||
}
|
||||
echo json_encode(
|
||||
[
|
||||
'mode' => $apply ? 'APPLIQUE' : 'SIMULATION',
|
||||
'backup' => $backup['name'] ?? null,
|
||||
'summary' => $summary,
|
||||
'groups' => $details,
|
||||
],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
),
|
||||
"\n";
|
||||
} catch (Throwable $error) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
fwrite(STDERR, 'Fusion interrompue : ' . $error->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue