Visits counter without database with PHP

You can use flock() which will lock the file so that other processes are not writing to the file.

Edit: updated to use fread() instead of include()

$fp = fopen("counter.txt", "r+");

while(!flock($fp, LOCK_EX)) {  // acquire an exclusive lock
    // waiting to lock the file
}

$counter = intval(fread($fp, filesize("counter.txt")));
$counter++;

ftruncate($fp, 0);      // truncate file
fwrite($fp, $counter);  // set your data
fflush($fp);            // flush output before releasing the lock
flock($fp, LOCK_UN);    // release the lock

fclose($fp);

<?php 

   /** 
   * Create an empty text file called counterlog.txt and  
   * upload to the same directory as the page you want to  
   * count hits for. 
   *  
   * Add this line of code on your page: 
   * <?php include "text_file_hit_counter.php"; ?> 
   */ 

  // Open the file for reading 
  $fp = fopen("counterlog.txt", "r"); 

  // Get the existing count 
  $count = fread($fp, 1024); 

  // Close the file 
  fclose($fp); 

  // Add 1 to the existing count 
  $count = $count + 1; 

  // Display the number of hits 
  // If you don't want to display it, comment out this line    
  echo "<p>Page views:" . $count . "</p>"; 

  // Reopen the file and erase the contents 
  $fp = fopen("counterlog.txt", "w"); 

  fwrite($fp, $count); 

  // Close the file 
  fclose($fp); 

 ?> 

It sounds easy, but its really hard to solve. The reason are race-conditions.

What are race-conditions?
If you open a counter file, read the content, increment the hits and write the hits to the file content, many things can happen between all these steps through other visitors opening the same script on your website simultaneously. Think about the situation when the first visitors request (thread) writes "484049" hits to the counter file char by char and in the millisecond while "484" is written the second thread reads that value and increments it to "485" loosing most of your nice hits.

Do not use global locks!
Maybe you think about solving this issue by using LOCK_EX. By that the second thread needs to wait until the first one has finished writing to the file. But "waiting" is nothing you really want. This means every thread and I really mean every thread needs to wait for other threads. You only need some raging bots on your website, many visitors or a temporary i/o problem on your drive and nobody is able to load your website until all writes have been finished... and what happens if a visitor can not open your website... he will refresh it, causing new waiting/locking threads... bottleneck!

Use thread based locks
The only secure solution is to create instantly a new counter file for simultaneously running threads:

<?php
// settings
$count_path = 'count/';
$count_file = $count_path . 'count';
$count_lock = $count_path . 'count_lock';

// aquire non-blocking exlusive lock for this thread
// thread 1 creates count/count_lock0/
// thread 2 creates count/count_lock1/
$i = 0;
while (file_exists($count_lock . $i) || !@mkdir($count_lock . $i)) {
    $i++;
    if ($i > 100) {
        exit($count_lock . $i . ' writable?');
    }
}

// set count per thread
// thread 1 updates count/count.0
// thread 2 updates count/count.1
$count = intval(@file_get_contents($count_file . $i));
$count++;
//sleep(3);
file_put_contents($count_file . $i, $count);

// remove lock
rmdir($count_lock . $i);
?>

Now you have count/count.1, count/count.2, etc in your counter folder while count.1 will catch most of the hits. The reason for that is that race-conditions do not happen all the time. They happen only if two threads were simultaneously.

Note: If you see (much) more than 2 files this means your server is really slow compared to the amount of visitors you have.

If you now want the total hits, you need to tidy them up (in this example randomly):

<?php
// tidy up all counts (only one thread is able to do that)
if (mt_rand(0, 100) == 0) {
    if (!file_exists($count_lock) && @mkdir($count_lock)) {
        $count = intval(@file_get_contents($count_file . 'txt'));
        $count_files = glob($count_path . '*.*');
        foreach ($count_files as $file) {
            $i = pathinfo($file, PATHINFO_EXTENSION);
            if ($i == 'txt') {
                continue;
            }
            // do not read thread counts as long they are locked
            if (!file_exists($count_lock . $i) && @mkdir($count_lock . $i)) {
                $count += intval(@file_get_contents($count_file . $i));
                file_put_contents($count_file . $i, 0);
                rmdir($count_lock . $i);
            }
        }
        file_put_contents($count_file . 'txt', $count);
        rmdir($count_lock);
    }
}

// print counter
echo intval(@file_get_contents($count_file . 'txt'));
?>

P.S. enable sleep(3) and look into the counter folder to simulate a slow server and you see how fast the multiple count files are growing.

Tags:

Php

Counter