Navigation

Php

How to Handle File Locking

Prevent concurrent file access conflicts in PHP using flock() for safe multi-process file operations and data integrity protection.

Table Of Contents

Implementation

File locking prevents data corruption when multiple processes access the same file simultaneously. PHP's flock() function provides advisory locking that coordinates access between cooperating processes, ensuring data integrity in concurrent environments.

<?php

// Basic file locking for safe writes
function writeWithLock(string $filename, string $data): bool {
    $handle = fopen($filename, 'w');
    
    if (!$handle) {
        throw new RuntimeException("Cannot open file: $filename");
    }
    
    // Acquire exclusive lock
    if (!flock($handle, LOCK_EX)) {
        fclose($handle);
        throw new RuntimeException("Cannot acquire lock on: $filename");
    }
    
    $result = fwrite($handle, $data);
    
    // Lock is automatically released when file is closed
    fclose($handle);
    
    return $result !== false;
}

// Read with shared lock
function readWithLock(string $filename): string {
    $handle = fopen($filename, 'r');
    
    if (!$handle) {
        throw new RuntimeException("Cannot open file: $filename");
    }
    
    // Acquire shared lock (multiple readers allowed)
    if (!flock($handle, LOCK_SH)) {
        fclose($handle);
        throw new RuntimeException("Cannot acquire shared lock on: $filename");
    }
    
    $content = stream_get_contents($handle);
    fclose($handle);
    
    return $content;
}

// Advanced file locker class
class FileLocker {
    private $handle;
    private string $filename;
    private bool $locked = false;
    
    public function __construct(string $filename, string $mode = 'r+') {
        $this->filename = $filename;
        $this->handle = fopen($filename, $mode);
        
        if (!$this->handle) {
            throw new RuntimeException("Cannot open file: $filename");
        }
    }
    
    public function lock(int $operation = LOCK_EX, bool $nonBlocking = false): bool {
        $lockFlags = $operation;
        if ($nonBlocking) {
            $lockFlags |= LOCK_NB;
        }
        
        $this->locked = flock($this->handle, $lockFlags);
        return $this->locked;
    }
    
    public function unlock(): bool {
        if ($this->locked) {
            $result = flock($this->handle, LOCK_UN);
            $this->locked = false;
            return $result;
        }
        return true;
    }
    
    public function read(): string {
        if (!$this->handle) {
            throw new RuntimeException('File not open');
        }
        
        rewind($this->handle);
        return stream_get_contents($this->handle);
    }
    
    public function write(string $data): bool {
        if (!$this->handle) {
            throw new RuntimeException('File not open');
        }
        
        rewind($this->handle);
        ftruncate($this->handle, 0);
        return fwrite($this->handle, $data) !== false;
    }
    
    public function append(string $data): bool {
        if (!$this->handle) {
            throw new RuntimeException('File not open');
        }
        
        fseek($this->handle, 0, SEEK_END);
        return fwrite($this->handle, $data) !== false;
    }
    
    public function __destruct() {
        if ($this->locked) {
            $this->unlock();
        }
        
        if ($this->handle) {
            fclose($this->handle);
        }
    }
}

// Safe counter implementation
class SafeCounter {
    private string $counterFile;
    
    public function __construct(string $counterFile) {
        $this->counterFile = $counterFile;
        
        if (!file_exists($counterFile)) {
            file_put_contents($counterFile, '0');
        }
    }
    
    public function increment(): int {
        $locker = new FileLocker($this->counterFile, 'r+');
        
        if (!$locker->lock(LOCK_EX)) {
            throw new RuntimeException('Cannot acquire lock for counter');
        }
        
        $current = (int)$locker->read();
        $new = $current + 1;
        $locker->write((string)$new);
        
        return $new;
    }
    
    public function decrement(): int {
        $locker = new FileLocker($this->counterFile, 'r+');
        
        if (!$locker->lock(LOCK_EX)) {
            throw new RuntimeException('Cannot acquire lock for counter');
        }
        
        $current = (int)$locker->read();
        $new = max(0, $current - 1);
        $locker->write((string)$new);
        
        return $new;
    }
    
    public function get(): int {
        $locker = new FileLocker($this->counterFile, 'r');
        
        if (!$locker->lock(LOCK_SH)) {
            throw new RuntimeException('Cannot acquire shared lock for counter');
        }
        
        return (int)$locker->read();
    }
}

// Log file with safe appending
class SafeLogger {
    private string $logFile;
    
    public function __construct(string $logFile) {
        $this->logFile = $logFile;
    }
    
    public function log(string $message, string $level = 'INFO'): bool {
        $timestamp = date('Y-m-d H:i:s');
        $logLine = "[$timestamp] [$level] $message\n";
        
        $handle = fopen($this->logFile, 'a');
        
        if (!$handle) {
            return false;
        }
        
        // Acquire exclusive lock for appending
        if (!flock($handle, LOCK_EX)) {
            fclose($handle);
            return false;
        }
        
        $result = fwrite($handle, $logLine);
        fclose($handle);
        
        return $result !== false;
    }
}

// Configuration manager with locking
class LockedConfig {
    private string $configFile;
    
    public function __construct(string $configFile) {
        $this->configFile = $configFile;
        
        if (!file_exists($configFile)) {
            file_put_contents($configFile, json_encode([]));
        }
    }
    
    public function get(string $key, mixed $default = null): mixed {
        $config = $this->readConfig();
        return $config[$key] ?? $default;
    }
    
    public function set(string $key, mixed $value): bool {
        $locker = new FileLocker($this->configFile, 'r+');
        
        if (!$locker->lock(LOCK_EX)) {
            return false;
        }
        
        $config = json_decode($locker->read(), true) ?: [];
        $config[$key] = $value;
        
        return $locker->write(json_encode($config, JSON_PRETTY_PRINT));
    }
    
    public function remove(string $key): bool {
        $locker = new FileLocker($this->configFile, 'r+');
        
        if (!$locker->lock(LOCK_EX)) {
            return false;
        }
        
        $config = json_decode($locker->read(), true) ?: [];
        unset($config[$key]);
        
        return $locker->write(json_encode($config, JSON_PRETTY_PRINT));
    }
    
    private function readConfig(): array {
        $locker = new FileLocker($this->configFile, 'r');
        
        if (!$locker->lock(LOCK_SH)) {
            return [];
        }
        
        $content = $locker->read();
        return json_decode($content, true) ?: [];
    }
}

// Atomic file operations
function atomicFileUpdate(string $filename, callable $updater): bool {
    $tempFile = $filename . '.tmp.' . uniqid();
    
    try {
        // Copy original to temp file
        if (file_exists($filename)) {
            if (!copy($filename, $tempFile)) {
                throw new RuntimeException('Cannot create temporary file');
            }
        } else {
            touch($tempFile);
        }
        
        $locker = new FileLocker($tempFile, 'r+');
        
        if (!$locker->lock(LOCK_EX)) {
            throw new RuntimeException('Cannot lock temporary file');
        }
        
        $content = $locker->read();
        $newContent = $updater($content);
        
        if (!$locker->write($newContent)) {
            throw new RuntimeException('Cannot write to temporary file');
        }
        
        $locker->unlock();
        unset($locker);
        
        // Atomic rename
        if (!rename($tempFile, $filename)) {
            throw new RuntimeException('Cannot rename temporary file');
        }
        
        return true;
        
    } catch (Exception $e) {
        if (file_exists($tempFile)) {
            unlink($tempFile);
        }
        throw $e;
    }
}

// Usage examples
try {
    // Basic file locking
    writeWithLock('data.txt', 'Important data that needs protection');
    $content = readWithLock('data.txt');
    echo "Read: $content\n";
    
    // Safe counter
    $counter = new SafeCounter('counter.txt');
    
    for ($i = 0; $i < 5; $i++) {
        $count = $counter->increment();
        echo "Counter: $count\n";
    }
    
    // Safe logging
    $logger = new SafeLogger('app.log');
    $logger->log('Application started');
    $logger->log('User logged in', 'INFO');
    $logger->log('Database error', 'ERROR');
    
    // Configuration management
    $config = new LockedConfig('settings.json');
    $config->set('database_host', 'localhost');
    $config->set('debug_mode', true);
    
    echo "DB Host: " . $config->get('database_host') . "\n";
    echo "Debug: " . ($config->get('debug_mode') ? 'enabled' : 'disabled') . "\n";
    
    // Atomic file update
    atomicFileUpdate('important.txt', function($content) {
        return $content . "\nNew line added safely";
    });
    
    // Advanced locking with timeout
    $locker = new FileLocker('shared_resource.txt', 'r+');
    
    if ($locker->lock(LOCK_EX | LOCK_NB)) {
        echo "Got exclusive lock immediately\n";
        $locker->write('Updated data');
    } else {
        echo "Could not get lock (non-blocking)\n";
    }
    
} catch (Exception $e) {
    echo "Locking error: " . $e->getMessage() . "\n";
}

Why This Works

File locking uses the operating system's advisory locking mechanism to coordinate access between processes. The flock() function provides four lock types: LOCK_SH (shared), LOCK_EX (exclusive), LOCK_UN (unlock), and LOCK_NB (non-blocking).

Shared locks allow multiple readers but prevent writers, while exclusive locks prevent both readers and writers. The non-blocking flag makes lock attempts return immediately rather than waiting, useful for avoiding deadlocks in complex scenarios.

Critical for applications with concurrent access patterns like web applications, background processors, and multi-user systems. Always combine with proper error handling and consider using atomic operations for critical data updates to ensure consistency.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Php