Solutions :
Either all changes happen, or none do (It avoids leaving the system in a half-changed state)
1 - Backup folder :
It script that if any step fails, everything is rolled back to its original state using a backup folder.
Rollback Logic : Backup the Original File and Directory, Creates a subfolder inside the backup folder, Copies the original file into that subfolder. Deletes the backup file and folder since everything succeeded.
<?php
//there is a folder that cannot be registered as an account.
$backupfolder = 'backupfolder';
$oldname = $_SESSION['oldname'];
$newname = $_POST['newname'];
$oldFile = "./$oldname/$oldname.php";
$newFile = "./$oldname/$newname.php";
$oldDir = "./$oldname";
$newDir = "./$newname";
mkdir("$backupFolder/$oldname");
copy($oldFile, "$backupFolder/$oldname/$oldname.php");
if (file_exists($oldFile) && rename($oldFile, $newFile)) {
if (file_exists($oldDir) && rename($oldDir, $newDir)) {
try {
$conn->beginTransaction();
$update1 = $conn->prepare("UPDATE table1 SET name=? WHERE name=?");
$update1->execute([$newname, $oldname]);
$update2 = $conn->prepare("UPDATE table2 SET name=? WHERE name=?");
$update2->execute([$newname, $oldname]);
if ($update1->rowCount() > 0 && $update2->rowCount() > 0) {
$conn->commit();
unlink("$backupFolder/$oldname/$oldname.php");
rmdir("$backupFolder/$oldname");
echo 'changed !';
} else {
throw new Exception();
}
} catch (Exception $e) {
$conn->rollBack();
rename($newDir, $oldDir);
copy("$backupFolder/$oldname/$oldname.php", $oldFile);
echo 'didnt change !';
}
} else {
copy("$backupFolder/$oldname/$oldname.php", $oldFile);
echo 'didnt change !';
}
} else {
copy("$backupFolder/$oldname/$oldname.php", $oldFile);
echo 'didnt change !';
}
?>
2 - Flags for Tracking Progress :
it uses a flags array to track which steps succeed.
Rollback Logic : If the database update fails, It checks if the directory was renamed and renames it back, It checks if the file was renamed and renames it back.
<?php
$oldname = $_SESSION['oldname'];
$newname = $_POST['newname'];
$oldFile = "./$oldname/$oldname.php";
$newFile = "./$oldname/$newname.php";
$oldDir = "./$oldname";
$newDir = "./$newname";
$flags = [
'file_renamed' => false,
'dir_renamed' => false,
'db_updated' => false
];
if (file_exists($oldFile) && rename($oldFile, $newFile)) {
$flags['file_renamed'] = true;
if (file_exists($oldDir) && rename($oldDir, $newDir)) {
$flags['dir_renamed'] = true;
try {
$conn->beginTransaction();
$update1 = $conn->prepare("UPDATE table1 SET name=? WHERE name=?");
$update1->execute([$newname, $oldname]);
$update2 = $conn->prepare("UPDATE table2 SET name=? WHERE name=?");
$update2->execute([$newname, $oldname]);
if ($update1->rowCount() > 0 && $update2->rowCount() > 0) {
$conn->commit();
$flags['db_updated'] = true;
echo 'changed !';
} else {
throw new Exception();
}
} catch (Exception $e) {
$conn->rollBack();
}
}
}
if (!$flags['db_updated']) {
if ($flags['dir_renamed']) {
rename($newDir, $oldDir);
}
if ($flags['file_renamed']) {
rename($newFile, $oldFile);
}
echo 'didnt change !';
}
?>
NOTE : The flags-based approach performs fewer physical operations on the file system, which means it's less prone to errors and generally faster.
with contributions from @adyson and @masoudiofficial