2
\$\begingroup\$

tmpfile() creates a temporary file which is automatically deleted when php exit. I wanted the same for directories, came up with:

/**
 * create a temporary directory that is automatically deleted when the script exits.
 * 
 * @throws RuntimeException on tmpfile()/mkdir() failure
 * @return string absolute path to the temporary directory
 */
function tmpdir(): string
{
 $max_attempts = 10;
 $attempts = 0;
 for (; $attempts < $max_attempts; ++$attempts) {
 $h = tmpfile();
 if ($h === false) {
 continue;
 }
 $tmpDirPath = stream_get_meta_data($h)['uri'];
 fclose($h);
 // there is technically a (unlikely) race condition between fclose() and mkdir(),
 // that's why i do the weird loop thing
 if (mkdir($tmpDirPath)) {
 break;
 }
 }
 if ($attempts >= $max_attempts) {
 throw new RuntimeException("tmpdir() failed: attempts: " . $attempts . ": error_get_last: " . var_export(error_get_last(), true));
 }
 static $deleteOnExitInstalled = false;
 static $tmpDirPaths = [];
 $tmpDirPaths[] = $tmpDirPath;
 if ($deleteOnExitInstalled === false) {
 register_shutdown_function(function () use (&$tmpDirPaths) {
 $rmdirRecursively = function ($dir) use (&$rmdirRecursively) {
 $children = glob($dir . DIRECTORY_SEPARATOR . '*', GLOB_NOSORT | GLOB_MARK);
 foreach ($children as $child) {
 $isChildDir = substr($child, -strlen(DIRECTORY_SEPARATOR)) === DIRECTORY_SEPARATOR;
 if ($isChildDir) {
 $rmdirRecursively(substr($child, 0, -strlen(DIRECTORY_SEPARATOR)));
 } else {
 unlink($child);
 }
 }
 rmdir($dir);
 };
 foreach ($tmpDirPaths as $tmpDirPath) {
 $rmdirRecursively($tmpDirPath);
 }
 });
 $deleteOnExitInstalled = true;
 }
 return $tmpDirPath;
}

sample usage:

$tmpDir = tmpdir();
file_put_contents("{$tmpDir}/test.txt", "Hello World!");
var_dump($tmpDir); // /tmp/phpSyEj2v
asked May 6, 2023 at 8:19
\$\endgroup\$
6
  • \$\begingroup\$ Can you explain why you want to create a temporary directory? All it could possibly contain are temporary files, but those you can already make. In other words: I am wondering if this is best solution for the problem you're trying to solve, but I don't know what that problem is. \$\endgroup\$ Commented May 7, 2023 at 14:40
  • \$\begingroup\$ @KIKOSoftware I want a temporary directory for a headless Chrome's --user-data-dir argument \$\endgroup\$ Commented May 7, 2023 at 15:46
  • \$\begingroup\$ OK, I get it. One thing I would do is make deleting a non-empty directory a separate function. It could be useful to a have a function like that, and not have to write one again. \$\endgroup\$ Commented May 7, 2023 at 17:22
  • \$\begingroup\$ I see how you are using static with $deleteOnExitInstalled to control its boolean value after subsequent calls of tmpdir(). Does $tmpDirPaths need this same behavior? It cannot start from scratch each time? Why not put $attempts = 0; in the first parameter of for()? Why not make $max_attempts = 10 the default parameter of tmpdir() to make it more easily customizable? Trivially, I'd use sprintf() with the runtimeException messge to manage the code line length. I might also cache the strlen() of the DS. \$\endgroup\$ Commented May 7, 2023 at 20:13
  • \$\begingroup\$ @mickmackusa Does $tmpDirPaths need this same behavior? It cannot start from scratch each time? - yes, the shutdown function use this array to keep track of all the temporary directories created. Why not put $attempts = 0; in the first parameter of for()? - in PHP it doesn't really matter, but in many other languages (javascript, C, C++, Rust, just off the top of my head) , variables created in the for loop, goes out of scope outside the for loop, so i have a habit of declaring variables used outside for loops, outside the for loop. (even though it's not required in PHP) \$\endgroup\$ Commented May 7, 2023 at 21:48

1 Answer 1

1
\$\begingroup\$

Found at least 1 bug: if the file contains symlinks, then

$isChildDir = substr($child, -strlen(DIRECTORY_SEPARATOR)) === DIRECTORY_SEPARATOR;

will incorrectly be true, resulting in the dir not being cleaned up and emitting

PHP Warning: rmdir(/tmp/phpOuCtAg/symlink): Not a directory in foo.php on line x

The fix seems to be

 foreach ($children as $child) {
 $isChildDir = substr($child, -strlen(DIRECTORY_SEPARATOR)) === DIRECTORY_SEPARATOR;
 if ($isChildDir) {
 $withoutTrailingSlash = substr($child, 0, -strlen(DIRECTORY_SEPARATOR));
 if (is_link($withoutTrailingSlash)) {
 unlink($withoutTrailingSlash);
 } else {
 $rmdirRecursively($withoutTrailingSlash);
 }
 } else {
 unlink($child);
 }
 }

I think that could probably be shortened further.

Toby Speight
87.3k14 gold badges104 silver badges322 bronze badges
answered Jul 18, 2023 at 11:31
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.