upd: DefaultController (#25)

add database transfer capability
This commit is contained in:
2026-03-26 15:14:15 -04:00
parent c948b1e39d
commit 2a22a3e027
5 changed files with 614 additions and 2 deletions

View File

@@ -0,0 +1,501 @@
<?php
namespace App\Service;
use App\Entity\Bible;
use App\Entity\Note;
use App\Entity\Reference;
use App\Entity\Series;
use App\Entity\Speaker;
use App\Entity\Template;
use App\Entity\User;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Persistence\ManagerRegistry;
use PDO;
use PDOException;
use Symfony\Component\Uid\Uuid;
class DatabaseTransferService
{
private PDO $srcDB;
private PDO $destDB;
public function __construct(
private string $projectDir,
private ManagerRegistry $doctrine
) {
// connect the source database
switch (explode(":", $_ENV['DATABASE_URL'])[0]) {
case 'pdo-sqlite';
list($driverName, $dbFile) = explode(":/", $_ENV['DATABASE_URL']);
$this->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;
}
}