This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
DataMigration/files/custom/Espo/Modules/DataMigration/Services/JunctionTableHandler.php
T
chaim e08eca7eb3 feat: initial DataMigration extension for case data backup/restore
Full case data migration tool with 3 console commands:
- data-migration-export: exports all case-related entities (27 types) + files
- data-migration-import: restores backup to target DB with dry-run support
- data-migration-status: shows entity counts from live DB or backup

Supports preserve-ids, skip-duplicates, batch processing, and attachment
file copying. Imports with hooks disabled to prevent side effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:50:46 +00:00

232 lines
7.1 KiB
PHP

<?php
namespace Espo\Modules\DataMigration\Services;
use Espo\ORM\EntityManager;
use Espo\Core\Console\IO;
use PDO;
class JunctionTableHandler
{
private const TEAM_ENTITY_TYPES = [
'Case', 'CaseActivity', 'Invoice', 'Charge',
'Document', 'SignatureRequest', 'CaseMemory', 'PricingAgreement',
];
public function __construct(
private EntityManager $entityManager
) {}
/**
* Export junction tables to JSON files.
*/
public function exportJunctions(
array $caseIds,
array $entityIdsByType,
string $outputDir,
?IO $io = null
): array {
$counts = [];
if (empty($caseIds)) {
return $counts;
}
$pdo = $this->entityManager->getPDO();
// case_contact
$inList = $this->buildInList($caseIds);
$counts['case_contact'] = $this->exportTableQuery(
$pdo,
'case_contact',
"SELECT * FROM `case_contact` WHERE `case_id` IN ({$inList}) AND `deleted` = 0",
$outputDir
);
// case_document
$counts['case_document'] = $this->exportTableQuery(
$pdo,
'case_document',
"SELECT * FROM `case_document` WHERE `case_id` IN ({$inList}) AND `deleted` = 0",
$outputDir
);
// entity_team - for all tracked entity types
$allTeamRecords = [];
foreach (self::TEAM_ENTITY_TYPES as $entityType) {
$ids = $entityIdsByType[$entityType] ?? [];
if (empty($ids)) {
continue;
}
$entityInList = $this->buildInList($ids);
$quotedType = $pdo->quote($entityType);
$sql = "SELECT * FROM `entity_team` WHERE `entity_type` = {$quotedType} AND `entity_id` IN ({$entityInList}) AND `deleted` = 0";
$sth = $pdo->query($sql);
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
$allTeamRecords[] = $row;
}
}
$filePath = rtrim($outputDir, '/') . '/_junction_entity_team.json';
file_put_contents(
$filePath,
json_encode([
'table' => 'entity_team',
'count' => count($allTeamRecords),
'exportedAt' => date('c'),
'records' => $allTeamRecords,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
$counts['entity_team'] = count($allTeamRecords);
if ($io) {
foreach ($counts as $table => $count) {
$io->writeLine(" Junction {$table}: {$count} records");
}
}
return $counts;
}
/**
* Import junction table records from backup.
*/
public function importJunctions(
string $inputDir,
IdMapper $idMapper,
bool $preserveIds = true,
?IO $io = null
): void {
$this->importCaseContact($inputDir, $idMapper, $preserveIds, $io);
$this->importCaseDocument($inputDir, $idMapper, $preserveIds, $io);
$this->importEntityTeam($inputDir, $idMapper, $preserveIds, $io);
}
private function exportTableQuery(PDO $pdo, string $table, string $sql, string $outputDir): int
{
$sth = $pdo->query($sql);
$records = $sth->fetchAll(PDO::FETCH_ASSOC);
$filePath = rtrim($outputDir, '/') . '/_junction_' . $table . '.json';
file_put_contents(
$filePath,
json_encode([
'table' => $table,
'count' => count($records),
'exportedAt' => date('c'),
'records' => $records,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
return count($records);
}
private function importCaseContact(string $inputDir, IdMapper $idMapper, bool $preserveIds, ?IO $io): void
{
$data = $this->readJunctionFile($inputDir, 'case_contact');
if ($data === null) {
return;
}
$pdo = $this->entityManager->getPDO();
$count = 0;
$stmt = $pdo->prepare(
"INSERT IGNORE INTO `case_contact` (`case_id`, `contact_id`, `role`, `party_order`, `deleted`) VALUES (?, ?, ?, ?, 0)"
);
foreach ($data['records'] as $row) {
$caseId = $preserveIds ? $row['case_id'] : ($idMapper->get('Case', $row['case_id']) ?? $row['case_id']);
$contactId = $preserveIds ? $row['contact_id'] : ($idMapper->get('Contact', $row['contact_id']) ?? $row['contact_id']);
$stmt->execute([$caseId, $contactId, $row['role'] ?? null, $row['party_order'] ?? null]);
$count++;
}
if ($io) {
$io->writeLine(" Junction case_contact: imported {$count} records");
}
}
private function importCaseDocument(string $inputDir, IdMapper $idMapper, bool $preserveIds, ?IO $io): void
{
$data = $this->readJunctionFile($inputDir, 'case_document');
if ($data === null) {
return;
}
$pdo = $this->entityManager->getPDO();
$count = 0;
$stmt = $pdo->prepare(
"INSERT IGNORE INTO `case_document` (`case_id`, `document_id`, `deleted`) VALUES (?, ?, 0)"
);
foreach ($data['records'] as $row) {
$caseId = $preserveIds ? $row['case_id'] : ($idMapper->get('Case', $row['case_id']) ?? $row['case_id']);
$documentId = $preserveIds ? $row['document_id'] : ($idMapper->get('Document', $row['document_id']) ?? $row['document_id']);
$stmt->execute([$caseId, $documentId]);
$count++;
}
if ($io) {
$io->writeLine(" Junction case_document: imported {$count} records");
}
}
private function importEntityTeam(string $inputDir, IdMapper $idMapper, bool $preserveIds, ?IO $io): void
{
$data = $this->readJunctionFile($inputDir, 'entity_team');
if ($data === null) {
return;
}
$pdo = $this->entityManager->getPDO();
$count = 0;
$stmt = $pdo->prepare(
"INSERT IGNORE INTO `entity_team` (`entity_id`, `entity_type`, `team_id`, `deleted`) VALUES (?, ?, ?, 0)"
);
foreach ($data['records'] as $row) {
$entityType = $row['entity_type'];
$entityId = $preserveIds ? $row['entity_id'] : ($idMapper->get($entityType, $row['entity_id']) ?? $row['entity_id']);
$teamId = $preserveIds ? $row['team_id'] : ($idMapper->get('Team', $row['team_id']) ?? $row['team_id']);
$stmt->execute([$entityId, $entityType, $teamId]);
$count++;
}
if ($io) {
$io->writeLine(" Junction entity_team: imported {$count} records");
}
}
private function readJunctionFile(string $inputDir, string $table): ?array
{
$path = rtrim($inputDir, '/') . '/_junction_' . $table . '.json';
if (!file_exists($path)) {
return null;
}
return json_decode(file_get_contents($path), true);
}
private function buildInList(array $ids): string
{
$pdo = $this->entityManager->getPDO();
return implode(',', array_map(fn($id) => $pdo->quote($id), $ids));
}
}