10

I have a single webpage and i would like to track how many times it's visited without using a database.

I thought about XML, updating a file every time a user visits the page:

<?xml version='1.0' encoding='utf-8'?>
<counter>8</counter>

Then i thought it could have been a better idea to declare a PHP counter in a separate file and then update it everytime a user visits the page.

counter.php

<?php
    $counter = 0;
?>

update_counter.php:

<?php
    include "counter.php";
    $counter += 1;
    $var = "<?php\n\t\$counter = $counter;\n?>";
    file_put_contents('counter.php', $var);
?>

With this, everytime update_counter.php is visited, the variable in the counter.php file is incremented.

Anyway, i noticed that if the counter.php file has $counter = 5 and the update_counter.php file is visited by i.e. 1000 users at the exact same time, the file gets read 1000 times at the same time (so the the value 5 gets read in all requests) the counter.php file will be updated with value 5+1 (=6) instead of 1005.

Is there a way to make it work without using database?

BackSlash
  • 21,927
  • 22
  • 96
  • 136
  • You will want to look into `flock()` http://www.php.net/manual/en/function.flock.php – cmorrissey Aug 14 '13 at 16:07
  • @ChristopherMorrissey I really didn't know about this function, can you explain what it is and how you use it in an answer? – BackSlash Aug 14 '13 at 16:08
  • I think to use `flock()` you'll have to read the file rather than include it. It would add a little bit of code, but wouldn't be too difficult. The trouble might be in the fact that you must open the file before locking it, and potentially multiple users could open it before the lock was acquired (meaning multiple users would read the same initial value) – Michael Wheeler Aug 14 '13 at 16:11
  • @MichaelWheeler If a file gets locked, will that mean that if the file is opened, the request to open it by another script is denied? Or the other script will just wait for the lock to be released? – BackSlash Aug 14 '13 at 16:17
  • Adding this as an answer as I do not have enough reputation to comment - actually after ftruncate() in cmorrissey's answer, the following line needs to be inserted to ensure writing to the file at the start. Else, the extraneous nulls will prevent the desired result. rewind($fp); – taatparya Jan 14 '19 at 11:52

5 Answers5

9

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);
cmorrissey
  • 8,493
  • 2
  • 23
  • 27
  • @MichaelWheeler OOOPS! – cmorrissey Aug 14 '13 at 16:28
  • @ChristopherMorrissey I tried it, but with browsershots(a website-screenshot service) i can see the same visits amount in different screenshots, so it's not working for me – BackSlash Aug 14 '13 at 16:32
  • @BackSlash I have updated the code to use `fread()` if it doesn't work point us to the live url you are using – cmorrissey Aug 14 '13 at 16:45
  • 1
    Be warned though: This code will hang and potentially spam your error log if your webserver doesn't have the permission to access counter.txt! fopen() will return false and you will get lots of `PHP Warning: flock() expects parameter 1 to be resource` lines.. EDIT: I edited the code in your answer to catch the missing permission – Klaus Mar 10 '16 at 14:29
  • This will produce a bottleneck if you have many visitors as they have to wait until all other threads unlocked the file. You can test it on your own. Make a lock with `sleep(10)` and start a second script incrementing the counter. And if you have temporary slow writing because of very high cpu usage etc it will begin with much visitors. – mgutt Mar 27 '17 at 14:44
  • 2
    You MUST use rewind() after ftruncate(). So your code is currently not working. – Abraham Tugalov Jul 21 '19 at 17:40
  • Expanding on what @AbrahamTugalov said. If you don't use `rewind($fp);` will work up 2, and then reset to 1. Looking at counter.txt, it will fill it with null values like this: `^@^@^@^@^@1` – Jeff Dec 16 '19 at 16:35
  • Repeatedly calling `flock` in a loop here doesn't make sense. If you have permission to write to the file, `flock` will block until you can acquire a lock, and you don't need to loop. If you *don't* have permission, this will spew warnings infinitely instead of just emitting one. – Mark Amery Jan 13 '20 at 18:41
7
<?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); 

 ?> 
bitcodr
  • 1,395
  • 5
  • 21
  • 44
4

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.

Community
  • 1
  • 1
mgutt
  • 5,867
  • 2
  • 50
  • 77
0
<?php 
 $File = "counter.txt"; 
 //This is the text file we keep our count in, that we just made

 $handle = fopen($File, 'r+') ; 
 //Here we set the file, and the permissions to read plus write

 $data = fread($handle, 512) ; 
 //Actully get the count from the file

 $count = $data + 1;
 //Add the new visitor to the count

 print "You are visitor number ".$count; 
 //Prints the count on the page
?>
Veloncia
  • 117
  • 3
  • 13
0

The following works beautifully except for the large file size for too many visits.

file_put_contents('counter.txt', '1', FILE_APPEND);
echo '<h1>Hi, Page served ' . filesize('counter.txt') . ' times!</h1>';

However, after the file reaches 1000 or 1000000, simply create another file which counts that unit as well. Inelegance of the large size is matched by the performance that does not require locking.

taatparya
  • 31
  • 2
  • 3