srcDB = new PDO('sqlite:'.$dbFile); break; case 'pdo-mysql': case 'pdo-pgsql': $dsn = $this->convertDBURLString($_ENV['DATABASE_URL']); $this->srcDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']); } // connect the destination database switch(explode(':', $_ENV['XFER_DATABASE_URL'])[0]) { case 'pdo-sqlite': list($driverName, $dbFile) = explode(":/", $_ENV['XFER_DATABASE']); $this->destDB = new PDO('sqlite:'.$dbFile); break; case 'pdo-mysql': case 'pdo-pgsql': $dsn = $this->convertDBURLString($_ENV['XFER_DATABASE_URL']); $this->destDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']); } } public function createSchema(): void { $destEm = $this->doctrine->getManager('transfer'); $metadata = $destEm->getMetadataFactory()->getAllMetadata(); $tool = new SchemaTool($destEm); $tool->dropSchema($metadata); $tool->createSchema($metadata); } public function getEntityClasses(): array { return [ User::class, Bible::class, Reference::class, Template::class, Series::class, Speaker::class, Note::class ]; } public function transferUserTable(): int { $sql = "SELECT * FROM user"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO user (id, email, roles, password, name, meta_data, home_church_rss) ". "VALUES ". "(:id, :email, :roles, :password, :name, :meta_data, :home_church)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $params = [ 'id' => $row['id'], 'email' => $row['email'], 'roles' => $row['roles'], 'password' => $row['password'], 'name' => $row['name'], 'meta_data' => $row['meta_data'], 'home_church' => $row['home_church_rss'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function transferBibleTable(): int { $sql = "SELECT * FROM bible"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO bible (id, book, chapter, verse, content, book_index, label) ". "VALUES ". "(:id, :book, :chapter, :verse, :content, :book_index, :label)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $uuid = Uuid::fromString($row['id']); $params = [ 'id' => $uuid->toBinary(), 'book' => $row['book'], 'chapter' => $row['chapter'], 'verse' => $row['verse'], 'content' => $row['content'], 'book_index' => $row['book_index'], 'label' => $row['label'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function transferReferenceTable(): int { $sql = "SELECT * FROM reference"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO reference (id, type, name, label, ndx, content) ". "VALUES ". "(:id, :type, :name, :label, :ndx, :content)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $params = [ 'id' => $row['id'], 'type' => $row['type'], 'name' => $row['name'], 'label' => $row['label'], 'ndx' => $row['ndx'], 'content' => $row['content'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function transferTemplateTable(): int { $sql = "SELECT * FROM template"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO template (id, user_id, name, content) ". "VALUES ". "(:id, :user_id, :name, :content)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $params = [ 'id' => $row['id'], 'user_id' => $row['user_id'], 'name' => $row['name'], 'content' => $row['content'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function transferSpeakerTable(): int { $sql = "SELECT * FROM speaker"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO speaker (id, name, user_id) ". "VALUES ". "(:id, :name, :user_id)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $params = [ 'id' => $row['id'], 'name' => $row['name'], 'user_id' => $row['user_id'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function transferSeriesTable(): int { $sql = "SELECT * FROM series"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO series (id, name, user_id, template_id) ". "VALUES ". "(:id, :name, :user_id, :template_id)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $params = [ 'id' => $row['id'], 'name' => $row['name'], 'user_id' => $row['user_id'], 'template_id' => $row['template_id'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function transferNoteTable(): int { $sql = "SELECT * FROM note"; $stmt = $this->srcDB->prepare($sql); $stmt->execute(); $insQuery = "INSERT INTO note (id, title, date, passage, refs, text, speaker_id, series_id, user_id, recording) ". "VALUES ". "(:id, :title, :date, :passage, :refs, :text, :speaker_id, :series_id, :user_id, :recording)"; $destStmt = $this->destDB->prepare($insQuery); $skippedCount = 0; $this->destDB->beginTransaction(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $params = [ 'id' => $row['id'], 'speaker_id' => $row['speaker_id'], 'series_id' => $row['series_id'], 'user_id' => $row['user_id'], 'title' => $row['title'], 'date' => $row['date'], 'passage' => $row['passage'], 'refs' => $row['refs'], 'text' => $row['text'], 'recording' => $row['recording'], ]; $this->destDB->exec("SAVEPOINT row_insert"); try { $destStmt->execute($params); $this->destDB->exec("RELEASE SAVEPOINT row_insert"); } catch (PDOException $e) { $skipCodes = [ '23000', '23503', '23505', ]; if (in_array($e->getCode(), $skipCodes, true)) { $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert"); $skippedCount++; // 4. Increment the counter continue; } $this->destDB->rollBack(); print_r($params); die("Critical Error: ".$e->getMessage()); } } $this->destDB->commit(); return $skippedCount; } public function finalizeEnvSwap(): void { $envPath = $this->projectDir . '/.env'; if (!file_exists($envPath)) { throw new \Exception(".env file not found at $envPath"); } $lines = file($envPath); $newLines = []; $xferUrlValue = null; // 1. First pass: Find the value of XFER_DATABASE_URL foreach ($lines as $line) { if (preg_match('/^XFER_DATABASE_URL=(.*)/', trim($line), $matches)) { $xferUrlValue = $matches[1]; break; } } if (!$xferUrlValue) { throw new \Exception("XFER_DATABASE_URL not found in .env file."); } // 2. Second pass: Perform the swap foreach ($lines as $line) { $trimmed = trim($line); // Prepend #OLD_ to the existing DATABASE_URL if (preg_match('/^DATABASE_URL=/', $trimmed)) { $newLines[] = "# OLD_" . $line; continue; } // Rename XFER_DATABASE_URL to DATABASE_URL if (preg_match('/^XFER_DATABASE_URL=/', $trimmed)) { $newLines[] = "DATABASE_URL=" . $xferUrlValue . PHP_EOL; continue; } $newLines[] = $line; } file_put_contents($envPath, implode('', $newLines)); } public static function isTransferEnabled(): bool { return (isset($_ENV['XFER_DATABASE_URL']) && !empty($_ENV['XFER_DATABASE_URL'])); } private function convertDBURLString(string $DBURL): array { $dsn = preg_split("/[:|\/|\@]+/", $DBURL); if (count($dsn) > 6) { die('You cannot have special characters in your password for the database entry during this process. Change the database user password to just use alpha-numeric characters, then run again. You can change it back to more secure after this transfer is complete'); } $res = [ 'dsn' => substr($dsn[0], 4).':'. 'host='.$dsn[3].';'. 'port='.$dsn[4].';'. 'dbname='.$dsn[5], 'username' => $dsn[1], 'password' => $dsn[2] ]; return $res; } }