file_put_contents($path, $data, FILE_APPEND | LOCK_EX) opens, locks, appends and closes in one call. With a handle, the fopen() mode decides what happens:
| Mode | Existing file | Missing file | Position |
|---|---|---|---|
| r (read only), r+ | kept | error | start |
| w, w+ | truncated | created | start |
| a, a+ | kept | created | end, for every write |
| x, x+ | error | created | start |
| c, c+ | kept | created | start |
x is an atomic "create only if absent"; c does not truncate, so you can lock before emptying the file, which w does on open. This script increments a shared counter:
<?php
declare(strict_types=1);
[, $mode, $times] = $argv;
for ($i = 0; $i < (int) $times; $i++) {
$fp = fopen(__DIR__ . '/hits.txt', 'c+');
if ($mode === 'lock') flock($fp, LOCK_EX); // wait until the lock is ours
$count = (int) stream_get_contents($fp);
ftruncate($fp, 0); rewind($fp);
fwrite($fp, (string) ($count + 1));
fclose($fp); // flushes, then releases the lock
}for mode in none lock; do
echo 0 > hits.txt
php counter.php $mode 5000 & php counter.php $mode 5000 &
wait
echo "$mode: expected 10000, got $(cat hits.txt)"
doneOutput
none: expected 10000, got 475 lock: expected 10000, got 10000
Unlocked, a process often reads just after the other truncated, gets an empty string and restarts from zero. flock() locks are advisory: they bind only processes that also call flock(), and end with LOCK_UN, fclose() or exit. LOCK_SH lets readers share, and LOCK_EX | LOCK_NB fails at once instead of waiting, so a cron job can skip a run while the last is busy.