0

Temporary files and tmpfile()

Beginner5 min read·php-13-005

Concept

File locking prevents race conditions when multiple PHP processes read or write the same file concurrently. Without locks, concurrent writes can interleave, producing corrupt files. Concurrent reads of a partially-written file can produce garbage.

flock(resource $handle, int $operation): PHP's file locking function. Works on any open file handle.

  • LOCK_SH (shared lock): Multiple processes can hold shared locks simultaneously. Use before reading.
  • LOCK_EX (exclusive lock): Only one process can hold an exclusive lock. Blocks until acquired. Use before writing.
  • LOCK_UN (unlock): Release the lock. Also released when the file handle is closed.
  • LOCK_NB (non-blocking): Bitwise OR with LOCK_SH or LOCK_EX to return immediately (false) instead of blocking if the lock is unavailable.

Advisory locks: flock uses advisory (voluntary) locks. Processes that don't call flock at all are not blocked — they can still read and write concurrently. All cooperating processes must use flock for locking to work.

file_put_contents with LOCK_EX: Internally uses flock(LOCK_EX) before writing. The simplest way to do a safe concurrent write without explicit handle management.

Deadlock: If process A holds LOCK_EX on file X and tries to acquire LOCK_EX on file Y, while process B holds LOCK_EX on file Y and tries to acquire LOCK_EX on file X — deadlock. Avoid by always acquiring locks in the same order, or by using LOCK_NB with a retry loop and timeout.

NFS: flock may not work reliably on NFS (Network File System). For distributed locking, use Redis (via SET key value NX PX timeout) or a database.

Code Example

php
<?php
declare(strict_types=1);

// Exclusive write with flock
function safeWrite(string $path, string $content): void
{
    $handle = fopen($path, 'c'); // 'c' — open for write, don't truncate yet
    if ($handle === false) {
        throw new \RuntimeException("Cannot open: $path");
    }

    try {
        if (!flock($handle, LOCK_EX)) {
            throw new \RuntimeException("Cannot acquire lock: $path");
        }

        // Truncate after acquiring lock
        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, $content);
        fflush($handle);     // flush PHP buffer to OS
        flock($handle, LOCK_UN); // explicit unlock (also unlocks on fclose)
    } finally {
        fclose($handle);
    }
}

// Non-blocking lock attempt with retry
function tryWrite(string $path, string $content, int $maxRetries = 5): bool
{
    $handle = fopen($path, 'c');
    if ($handle === false) return false;

    try {
        $attempts = 0;
        while (!flock($handle, LOCK_EX | LOCK_NB)) {
            if (++$attempts >= $maxRetries) {
                return false; // give up
            }
            usleep(100_000); // 100ms
        }

        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, $content);
        flock($handle, LOCK_UN);
        return true;
    } finally {
        fclose($handle);
    }
}

// Shared read lock — safe concurrent reads
function safeRead(string $path): string
{
    $handle = fopen($path, 'r');
    if ($handle === false) {
        throw new \RuntimeException("Cannot open: $path");
    }

    try {
        flock($handle, LOCK_SH); // allow other readers, block writers
        $content = stream_get_contents($handle);
        flock($handle, LOCK_UN);
        return $content;
    } finally {
        fclose($handle);
    }
}

// Simplest safe append (file_put_contents handles locking internally)
file_put_contents('/var/log/app.log', "Log entry\n", FILE_APPEND | LOCK_EX);