Navigation

Php

How to Monitor File Changes

Track file modifications, additions, and deletions in PHP using filesystem monitoring techniques for automated processing and change detection.

Table Of Contents

The Answer

File monitoring enables real-time detection of filesystem changes for automated workflows. PHP provides several approaches from simple timestamp checking to advanced inotify integration, allowing applications to respond immediately to file system events.

<?php

// Simple file modification monitor
class FileMonitor {
    private array $watchedFiles = [];
    private array $lastModified = [];
    
    public function watch(string $filepath): self {
        if (!file_exists($filepath)) {
            throw new InvalidArgumentException("File not found: $filepath");
        }
        
        $this->watchedFiles[] = $filepath;
        $this->lastModified[$filepath] = filemtime($filepath);
        
        return $this;
    }
    
    public function checkChanges(): array {
        $changes = [];
        
        foreach ($this->watchedFiles as $file) {
            if (!file_exists($file)) {
                $changes[] = [
                    'file' => $file,
                    'action' => 'deleted',
                    'timestamp' => time()
                ];
                continue;
            }
            
            $currentModTime = filemtime($file);
            
            if ($currentModTime > $this->lastModified[$file]) {
                $changes[] = [
                    'file' => $file,
                    'action' => 'modified',
                    'old_time' => $this->lastModified[$file],
                    'new_time' => $currentModTime,
                    'timestamp' => time()
                ];
                
                $this->lastModified[$file] = $currentModTime;
            }
        }
        
        return $changes;
    }
    
    public function startMonitoring(callable $changeHandler, int $intervalSeconds = 1): void {
        while (true) {
            $changes = $this->checkChanges();
            
            foreach ($changes as $change) {
                $changeHandler($change);
            }
            
            sleep($intervalSeconds);
        }
    }
}

// Directory monitor for new files
class DirectoryMonitor {
    private array $watchedDirs = [];
    private array $knownFiles = [];
    
    public function watchDirectory(string $directory): self {
        if (!is_dir($directory)) {
            throw new InvalidArgumentException("Directory not found: $directory");
        }
        
        $this->watchedDirs[] = $directory;
        $this->knownFiles[$directory] = $this->scanDirectory($directory);
        
        return $this;
    }
    
    private function scanDirectory(string $directory): array {
        $files = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
        );
        
        foreach ($iterator as $file) {
            $files[$file->getPathname()] = [
                'size' => $file->getSize(),
                'modified' => $file->getMTime(),
                'type' => $file->isFile() ? 'file' : 'directory'
            ];
        }
        
        return $files;
    }
    
    public function detectChanges(): array {
        $allChanges = [];
        
        foreach ($this->watchedDirs as $directory) {
            $currentFiles = $this->scanDirectory($directory);
            $knownFiles = $this->knownFiles[$directory];
            
            // Detect new files
            foreach ($currentFiles as $filepath => $info) {
                if (!isset($knownFiles[$filepath])) {
                    $allChanges[] = [
                        'file' => $filepath,
                        'action' => 'created',
                        'type' => $info['type'],
                        'size' => $info['size'],
                        'timestamp' => time()
                    ];
                }
            }
            
            // Detect deleted files
            foreach ($knownFiles as $filepath => $info) {
                if (!isset($currentFiles[$filepath])) {
                    $allChanges[] = [
                        'file' => $filepath,
                        'action' => 'deleted',
                        'type' => $info['type'],
                        'timestamp' => time()
                    ];
                }
            }
            
            // Detect modified files
            foreach ($currentFiles as $filepath => $info) {
                if (isset($knownFiles[$filepath])) {
                    $known = $knownFiles[$filepath];
                    
                    if ($info['modified'] > $known['modified'] || 
                        $info['size'] !== $known['size']) {
                        $allChanges[] = [
                            'file' => $filepath,
                            'action' => 'modified',
                            'type' => $info['type'],
                            'old_size' => $known['size'],
                            'new_size' => $info['size'],
                            'timestamp' => time()
                        ];
                    }
                }
            }
            
            $this->knownFiles[$directory] = $currentFiles;
        }
        
        return $allChanges;
    }
}

// Configuration file watcher
function watchConfigFile(string $configFile, callable $reloadCallback): void {
    $lastModified = filemtime($configFile);
    
    while (true) {
        clearstatcache(); // Clear filesystem cache
        
        if (!file_exists($configFile)) {
            error_log("Config file deleted: $configFile");
            break;
        }
        
        $currentModified = filemtime($configFile);
        
        if ($currentModified > $lastModified) {
            try {
                $reloadCallback($configFile);
                $lastModified = $currentModified;
                echo "Config reloaded: " . date('Y-m-d H:i:s') . "\n";
            } catch (Exception $e) {
                error_log("Config reload failed: " . $e->getMessage());
            }
        }
        
        sleep(1);
    }
}

// Log file tail monitor
function monitorLogFile(string $logFile, callable $lineProcessor): void {
    $handle = fopen($logFile, 'r');
    if (!$handle) {
        throw new RuntimeException("Cannot open log file: $logFile");
    }
    
    // Seek to end of file
    fseek($handle, 0, SEEK_END);
    
    while (true) {
        $line = fgets($handle);
        
        if ($line !== false) {
            $lineProcessor(trim($line));
        } else {
            // No new data, check if file was rotated
            clearstatcache();
            $currentSize = filesize($logFile);
            $currentPos = ftell($handle);
            
            if ($currentSize < $currentPos) {
                // File was truncated/rotated
                fclose($handle);
                $handle = fopen($logFile, 'r');
                fseek($handle, 0, SEEK_END);
            } else {
                usleep(100000); // 100ms
            }
        }
    }
    
    fclose($handle);
}

// Batch file processor with monitoring
function processFilesOnChange(string $watchDir, string $pattern, callable $processor): void {
    $processed = [];
    
    while (true) {
        $files = glob($watchDir . DIRECTORY_SEPARATOR . $pattern);
        
        foreach ($files as $file) {
            $fileId = $file . '_' . filemtime($file);
            
            if (!in_array($fileId, $processed)) {
                try {
                    $processor($file);
                    $processed[] = $fileId;
                    echo "Processed: " . basename($file) . "\n";
                } catch (Exception $e) {
                    error_log("Processing failed for $file: " . $e->getMessage());
                }
            }
        }
        
        // Clean up old processed files list
        if (count($processed) > 1000) {
            $processed = array_slice($processed, -500);
        }
        
        sleep(5);
    }
}

// Change event logger
function logFileChanges(string $watchPath, string $logFile): void {
    $monitor = is_dir($watchPath) ? new DirectoryMonitor() : new FileMonitor();
    
    if (is_dir($watchPath)) {
        $monitor->watchDirectory($watchPath);
    } else {
        $monitor->watch($watchPath);
    }
    
    while (true) {
        $changes = is_dir($watchPath) ? 
                  $monitor->detectChanges() : 
                  $monitor->checkChanges();
        
        foreach ($changes as $change) {
            $logEntry = sprintf(
                "[%s] %s: %s (%s)\n",
                date('Y-m-d H:i:s'),
                strtoupper($change['action']),
                $change['file'],
                $change['type'] ?? 'file'
            );
            
            file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
        }
        
        sleep(2);
    }
}

// Usage examples
try {
    // Monitor specific files
    $fileMonitor = new FileMonitor();
    $fileMonitor->watch('config.php')
               ->watch('settings.json');
    
    $changes = $fileMonitor->checkChanges();
    foreach ($changes as $change) {
        echo "File {$change['action']}: {$change['file']}\n";
    }
    
    // Monitor directory for new uploads
    $dirMonitor = new DirectoryMonitor();
    $dirMonitor->watchDirectory('uploads/');
    
    $dirChanges = $dirMonitor->detectChanges();
    foreach ($dirChanges as $change) {
        echo "Directory change: {$change['action']} - {$change['file']}\n";
    }
    
    // Watch config file and reload on change
    // watchConfigFile('app.conf', function($configFile) {
    //     echo "Reloading configuration from: $configFile\n";
    //     // Reload application configuration here
    // });
    
    // Monitor log file for new entries
    // monitorLogFile('app.log', function($line) {
    //     if (str_contains($line, 'ERROR')) {
    //         echo "Error detected: $line\n";
    //         // Send alert or take action
    //     }
    // });
    
    // Process new files automatically
    // processFilesOnChange('inbox/', '*.txt', function($file) {
    //     echo "Processing new file: $file\n";
    //     // Process the file here
    //     rename($file, 'processed/' . basename($file));
    // });
    
} catch (Exception $e) {
    echo "Monitoring error: " . $e->getMessage() . "\n";
}

Behind the Scenes

File monitoring works by comparing current filesystem state with previously recorded state. The most basic approach uses filemtime() to detect modification time changes, while more advanced methods track file sizes, content hashes, and directory listings.

For real-time monitoring, PHP applications typically use polling loops with sleep intervals to balance responsiveness with system resource usage. On Linux systems, the inotify extension provides event-driven monitoring with better performance and immediate notification of changes.

Key considerations include handling file rotation (logs), dealing with temporary files created during saves, and managing memory usage when monitoring large directory trees. Essential for automated processing systems, configuration management, and real-time data processing applications.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Php