Temporary files and tmpfile()
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
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);