Best Practices Database Transactions and Saving Files in a File System in PHP

What would be best practice for integrity if the user uploads the user data along with the file in which the user data is stored in the database and the file is stored in the file system?

For the moment, I will do something like the following code snippet using PHP and PDO (the code has not been verified, but I hope you get my opinion). I do not like the part of the save image in the User :: insert method. Is there a good way around this?

<?php User::insert($name, $image, $ext); Class User{ static public function insert($name, $image, $ext){ $conn = DB_config::get(); $conn->beginTransaction(); $sth = $conn->prepare(" INSERT INTO users (name) values(:name) ;"); $sth->execute(array( ":name" => $name )); if ($conn->lastInsertId() > -1 && Image::saveImage($image, IMAGE_PATH . $conn->lastInsertId(). $ext)) $conn->commit(); else $conn->rollback(); return $conn->lastInsertId(); } } Class Image{ static public function saveimage($image, $filename){ $ext = self::getExtensionFromFilename($filename); switch($ext){ case "jpg": case "jpeg": return imagejpeg(imagecreatefromstring($image), $filename); } return false; } ?> 
+6
source share
4 answers

Try it.

  • Save the image to disk in the workspace. The best thing is to keep the workspace at the same level as the final destination. It is also best to place it in a separate directory.

  • Start the transaction with the database.

  • Insert your user.

  • Rename the image file after the user ID.

  • Commit transaction.

What does this mean, he first performs the most risky operation, saving the image. All sorts of things can happen here: the system may fail, the disk may fill up, the connection may close. This is (most likely) the most time-consuming of your operations, so it is definitely the most risky.

Once this is done, you will begin the transaction and insert the user.

If the system does not work at this time, your insert will be rolled back, and the image will be in the temporary directory. But for your real system, "nothing happened" effectively. The temporary directory can be cleared using an automated function (i.e., Clear upon reboot, clear everything in X hours / days, etc.). Files must have a very short amount of time in this directory.

Then rename the image to its last place. File renames are atomic. They work, or they do not.

If the system is after this, the user line will be rolled back, but the file will be at the final destination. However, if after a reboot someone tries to add a new user who has the same user ID as the one that failed, their downloaded image will simply overwrite the existing one - do no harm, not a foul. If the user ID cannot be reused, you will have a lost image. But it can reasonably be cleaned once a week or once a month through an automatic procedure.

Finally, complete the transaction.

At this point, everything is in the right place.

+5
source

you can make such a class if you change the image and user classes to an implied interface ...

 class Upload { public static function performUpload($name, $image, $ext) { $user = new User($name); $user->save(); $img = new Image($image, $ext); $img->save(); $isValid = $user->isValid() && $image->isValid(); if (!$isValid) { $user->delete(); $img->delete(); } return $isValid; } } 
+2
source

This seems like the perfect time to use a try / catch block to control thread execution. It also seems that you are missing a large part of the puzzle, which is to save the image path created during image saving, to the user within the user's table.

The following code has not been verified, but should put you on the correct track:

 Class User{ static public function insert($name, $image, $ext) { $conn = DB_config::get(); // This will force any PDO errors to throw an exception, so our following t/c block will work as expected // Note: This should be done in the DB_config::get method so all api calls to get will benefit from this attribute $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); try { $conn->beginTransaction(); $sth = $conn->prepare(" INSERT INTO users (name) values(:name);" ); $sth->execute(array(":name" => $name)); $imagePath = Image::saveImage($image, IMAGE_PATH . $conn->lastInsertId(). $ext)); // Image path is an key component of saving a user, so if not saved lets throw an exception so we don't commit the transaction if (false === $imagePath) { throw new Exception(sprintf('Invalid $imagePath: %s', $imagePath)); } $sth = $conn->prepare("UPDATE users SET image_path = :imagePath WHERE id = :userId LIMIT 1"); $sth->bindValue(':imagePath', $imagePath, PDO::PARAM_STR); $sth->bindValue(':userId', $conn->lastInsertId(), PDO::PARAM_INT); $sth->execute(); // If we made this far and no exception has been thrown, we can commit our transaction $conn->commit(); return $conn->lastInsertId(); } catch (Exception $e) { error_log(sprintf('Error saving user: %s', $e->getMessage())); $conn->rollback(); } return 0; } } 
+2
source

I think you should use the command template and first invoke file operations, right after that database operation. Thus, you can use the database rollback of transactions and write a manual rollback for file operations, for example, you can save the contents of a file in memory or in temporary storage if something does not work ... It is much easier to roll back files, and then manually rolling back database records ...

Ohh and block resources are always in the same order if you do not want a dead end ... For example, lock files are always in ABC order and always use the database after file operations. By the way, in rare cases, you can use file system transactions. It depends on your server file system ...

0
source

Source: https://habr.com/ru/post/921345/


All Articles