I managed to make this PHP counter without database, it is very basic as it increments the visits in a .txt file:
$counter_file = ("count.txt");
$fp = fopen($counter_file, "r");
$count = fread($fp, 1024);
fclose($fp);
$count = $count +1;
$fp = fopen($counter_file, "w");
fwrite($fp, $count);
fclose($fp);
But this counter fails on a distant server, when the visits are too fast. It goes back to 0.
What can explain this behaviour and how to make sure the counter will never go back to 0?
Edit: This script seams to be more robust. It uses flock as #ghopst suggested.
$counter_file = ("count.txt");
$handle = fopen($counter_file,"r+");
//Lock File, error if unable to lock
if(flock($handle, LOCK_EX)) {
$count = fread($handle, filesize($counter_file));
$count = $count + 1;
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, $count);
flock($handle, LOCK_UN);
} else {
echo "Could not Lock File!";
}
fclose($handle);
It's down to the file system, your code makes a request to open, read, close, open, write and close the file for each visitor. If the file is being written it is locked against being written to by another instance, it's a behavior of the file system. Perhaps it would be better to have a simple database table with a autoincrement column and just insert a row for each visit then delete it , then you could just select the top row to return a value.
Try this version:
<?php
$counter_file = ("count.txt");
$count = #file_get_contents($counter_file);
$count = $count ? intval($count) + 1 : 1;
file_put_contents($counter_file, $count);
Related
I developed a very simple counter in PHP. It works as expected but occasionally it resets to zero. No idea why. I suspect it could be related to concurrent visitors but I have no idea how to prevent that in case I am correct. Here is the code:
function updateCounter($logfile) {
$count = (int)file_get_contents($logfile);
$file = fopen($logfile, 'w');
if (flock($file, LOCK_EX)) {
$count++ ;
fwrite($file, $count);
flock($file, LOCK_UN);
}
fclose($file);
return number_format((float)$count, 0, ',', '.') ;
}
Thank you in advance.
file_get_contents on a locked file will probably get a "false" (== 0) and the logfile is probably unlocked again, when it comes to writing.
A classic race condition...
As file_get_contents() can return false accessing a previously locked file, the consequent fwrite() may write a zero or 1, resetting our counter to zero.
So we try to read the counter file after the locking has been succeeded for us.
function updateCounter($logfile) {
//$count = (int)file_get_contents($logfile);
if(file_exists($logfile)) {
$mode = 'r+';
} else {
$mode = 'w+';
}
//
$file = fopen($logfile, $mode);
//
if (flock($file, LOCK_EX)) {
//
// read counter file:
//
$count = (int) fgets($file);
$count++ ;
//
// point to the beginning of the file:
//
rewind($file);
fwrite($file, $count);
flock($file, LOCK_UN);
}
fclose($file);
return number_format((float)$count, 0, ',', '.') ;
}
//
$logfile = "counter.log";
echo updateCounter($logfile);
Please see usernotes on https://www.php.net/manual/en/function.flock.php .
I would append a character into the file and use strlen on the file contents to get the hits. Please note that your file will get big overtime but this can be easily solved with a cronjob that sums it up and cache it into another readonly file.
You can also use !is_writeable and check if its locked and if so you can miss the hit or wait with a while loop until its writable. Tricky but it works. It depends how valuable each hit will be and how much effort you would like to invest in this counter.
When I run this function on multiple scripts one script generated warning:
fread(): Length parameter must be greater than 0
function test($n){
echo "<h4>$n at ".time()."</h4>";
for ($i = 0; $i<50; $i++ ){
$fp = fopen("$n.txt", "r");
$s = fread($fp, filesize("$n.txt") );
fclose($fp);
$fp = fopen("$n.txt", "w");
$s = $_SERVER['HTTP_USER_AGENT'].' '.time();
if (flock($fp, LOCK_EX)) { // acquire an exclusive lock
fwrite($fp, $s);
// fflush($fp);// flush output before releasing the lock
flock($fp, LOCK_UN); // release the lock
} else {
echo "Couldn't get the lock!";
}
}
}
I try to write reading of the file for multiple users, but only one user can write the file. I know that when I use fwrite with flock - LOC_EX, next scripts must wait till the write is finished. But here it seems like filesize doesn't wait till the write operation is finished. My opinion is that it tries to reach the file when the file size is 0, and as a result this produces the problem: 0 bytes will be read from the file, when it is written by original script.
Is it possible to fix this for fread function?
Purpose of this script is to test fread with some limit and to check the data which I read later, if the data are really written when I did not used fflush.
function test($n){
echo "<h4>$n at ".time()."</h4>";
for ($i = 0; $i<50; $i++ ){
$start = microtime(true);
$fp = fopen("$n.txt", "r");
if(filesize($n.txt) > 0)
{
$s = fread($fp, filesize($n.txt) );
fclose($fp);
$fp = fopen("$n.txt", "w");
$s = $_SERVER['HTTP_USER_AGENT'].' '.time();
if (flock($fp, LOCK_EX)) { // acquire an exclusive lock
fwrite($fp, $s);
// fflush($fp);// flush output before releasing the lock
flock($fp, LOCK_UN); // release the lock
} else {
echo "Couldn't get the lock!";
}
}
else
{
echo "Filesize must be greater than 0";
}
}
}
please change $s variables name its use same things two time
$fp = fopen("$n.txt", "r");
$s = fread($fp, filesize("$n.txt") );
fclose($fp);
The error occurs in the middle line of the above three lines.
Firstly, these three lines could be rewritten into a single line as follows:
$s = file_get_contents("$n.txt");
However, these isn't necessary, as these three lines are entirely redundant in your code. They don't do anything useful.
What they do is open a file, store its contents to $s and then close it.
But you are then immediately setting $s to a different value, thus throwing away the previous value, and making it pointless to have read it from the file in the first place.
If you need to keep the original contents of the file, then use file_get_contents() and make sure you don't overwrite the contents of the variable.
If you don't need the original contents of the file, then just delete those three lines from your code.
Incidentally, this error highlights a couple of good coding practices that you should take on board: Firstly, never re-use a variable for two different things, and secondly always give your variables (and functions) good names. $s is not a good name; $previousFileContents would be a better name; it would have made the error much more obvious.
Here is my Code with filename
it does work without problems if lets say i just use
update.php?pokemon=pikachu
it updates pikachu value in my found.txt +0.0001
But now my problem, when i have multiple threads running and randomly
2 threads are
update.php?pokemon=pikachu
and
update.php?pokemon=zaptos
i see the found.txt file
is empty than!!
so nothing is written in it then anymore.
So i guess its a bug when the php file is opened and another request is posted to the server.
How can i solve this problem this does accour often
found.txt
pikachu:2.2122
arktos:0
zaptos:0
lavados:9.2814
blabla:0
update.php
<?php
$file = "found.txt";
$fh = fopen($file,'r+');
$gotPokemon = $_GET['pokemon'];
$users = '';
while(!feof($fh)) {
$user = explode(':',fgets($fh));
$pokename = trim($user[0]);
$infound = trim($user[1]);
// check for empty indexes
if (!empty($pokename)) {
if ($pokename == $gotPokemon) {
if ($gotPokemon == "Pikachu"){
$infound+=0.0001;
}
if ($gotPokemon == "Arktos"){
$infound+=0.0001;
}
if ($gotPokemon == "Zaptos"){
$infound+=0.0001;
}
if ($gotPokemon == "Lavados"){
$infound+=0.0001;
}
}
$users .= $pokename . ':' . $infound;
$users .= "\r\n";
}
}
file_put_contents('found.txt', $users);
fclose($fh);
?>
I would create an exclusive lock after open the file and then release the lock before closing the file:
For creating an exclusive lock over the file:
flock($fh, LOCK_EX);
To delete it:
flock($fh, LOCK_UN);
Anyway you will need to check if other threads hot already the lock, so the first idea coming up is to try a few attempts to get the lock and if it's not finally possible, to inform the user, throw an exception or whatever other action to avoid an infinite loop:
$fh = fopen("found.txt", "w+");
$attempts = 0;
do {
$attempts++;
if ($attempts > 5) {
// throw exception or return response with http status code = 500
}
if ($attempts != 1) {
sleep(1);
}
} while (!flock($fh, LOCK_EX));
// rest of your code
file_put_contents('found.txt', $users);
flock($fh, LOCK_UN); // release the lock
fclose($fh);
Update
Probably the issue still remains because the reading part, so let's create also a shared lock before start reading and also let's simplify the code:
$file = "found.txt";
$fh = fopen($file,'r+');
$gotPokemon = $_GET['pokemon'];
$users = '';
$wouldblock = true;
// we add a shared lock for reading
$locked = flock($fh, LOCK_SH, $wouldblock); // it will wait if locked ($wouldblock = true)
while(!feof($fh)) {
// your code inside while loop
}
// we add an exclusive lock for writing
flock($fh, LOCK_EX, $wouldblock);
file_put_contents('found.txt', $users);
flock($fh, LOCK_UN); // release the locks
fclose($fh);
Let's see if it works
I am having this strange issue and can't figure it out.
On some websites I have this script works perfect... same code, same server settings...
With php, there is a simple page view hit counter that stores locally in a txt file.
Then I echo out the value on the footer copyright area of my websites to give the client a quick statistic... its pretty cool how fast it grows.
Anyway.. i have a client corner grill ny . com (seo purposes I added spaces )
On that website.. its been working great for years.
Now another website and a bunch more.. for example... savianos . com
This breaks.. and the text value is blank.
This is the counter.php code
<?php
session_start();
$counter_name = "counter/hits.txt";
//Check if a text file exists. If not create one and initialize it to zero.
if (!file_exists($counter_name)) {
$f = fopen($counter_name, "w");
fwrite($f,"0");
fclose($f);
}
// Read the current value of our counter file
$f = fopen($counter_name,"r");
$counterVal = fread($f, filesize($counter_name));
fclose($f);
// Has visitor been counted in this session?
// If not, increase counter value by one
if(!isset($_SESSION['hasVisited'])){
$_SESSION['hasVisited']="yes";
$counterVal++;
$f = fopen($counter_name, "w");
fwrite($f, $counterVal);
fclose($f);
}
?>
Now, if I add a value in the txt file.. like 1040... and go to the website it starts to work... then after a week or so I check it .. its blank again.
Any ideas?
I am thinking that this may be happening because the website might get a TON of views during dinner time friday night.. and the simple script can't handle it so.. while its trying to write a added a number it just breaks and go to blank.. and never starts back up again.
The structure is this.
/counter/ folder has
counter.php and a hits.txt file
Every page of the website the very first thing is
<?php include ('counter/counter.php'); ?>
and in the footer of the website we have
<?php echo $counterVal; ?>
Your code looks perfect, but let's understand the situation. You have a file which can be accessed concurrently for many users, because page visit can be done by multiple users on same time. This does't seem right you have to lock the file manipulation for another user while someone is modifying it, right?. Please have a look
Visits counter without database with PHP
It is most likely because you have two concurrent scripts that tried to open the file at one and one of them fail. You have to use flock() when there are multiple instances of the script that could operate at the same time. Counter are some of the heaviest things if you going to use file reading and writing. I wrote this wrapper to easily implement file locking.
If you want to check out one of my counters that in operation try http://ozlu.org. That dynamic counter image was self-built. The fileReadAll will read the entire file in one shot. The file writer only has two modes, write or append. You can pass the fileWriter an array or a string and it will write it to the file. The function will not add any \n to format your text so you would have to add that. The default mode for the fileWriteAll is w if you do not set the third argument.
function fileWriteAll($file, $content, $mode = "w"){
$mode = $mode === "w" || $mode === "a"? $mode : "w";
$FILE = fopen($file, $mode);
while (!flock($FILE, LOCK_EX)) { usleep(1); }
if( is_array($content) ){
for ($i = 0; $i < count($content); $i++){
fwrite($FILE, $content[$i]);
}
} else {
fwrite($FILE, $content);
}
flock($FILE, LOCK_UN);
fclose($FILE);
}
function fileReadAll($file){
$FILE = fopen($file, 'r');
while (!flock($FILE, LOCK_SH)) { usleep(1); }
$content = fread($FILE, filesize($file));
flock($FILE, LOCK_UN);
fclose($FILE);
return $content;
}
Your modified code:
session_start();
$counterName = './views.txt';
if (!file_exists($counterName)) {
$file = fopen($counterName, 'w');
fwrite($file, '0');
fclose($file);
}
$file = fopen($counterName, 'r');
$value = fread($file, filesize($counterName));
fclose($file);
if (! isset($_SESSION['visited'])) {
$_SESSION['visited'] = 'yes';
$value++;
$file = fopen($counterName, 'w');
fwrite($file, $value);
fclose($file);
}
session_unset();
echo $value;
i use this
<?php $handle = fopen("counter.txt", "r"); if(!$handle){
echo "could not open the file" ; } else {
$counter = (int ) fread($handle,20);
fclose ($handle);
$counter++;
echo" <strong> you are visitor no ". $counter . " </strong> " ; $handle = fopen("counter.txt", "w" ); fwrite($handle,$counter) ;
fclose ($handle) ;
} ?>
but its makes a counter for the page tst.php every entry.
i want to make counting for this page tst.php?id=1
but if i change to tst.php?id=2 to store this data to but from zero and counting.
and then return to tst,php?id=1 and showing the continuing counting of visits to this page.
and the another tst.php?id=....
for every id page!
You can try something like:
<?php
$const='counter';
$id=$_GET['id'];
$ext='.txt';
if(file_exists($const.$id.$ext))
{
$file=fopen($const.$id.$ext,'r');
$data=fread($file,filesize($const.$id.$ext));
fclose($file);
$file=fopen($const.$id.$ext,'w');
fwrite($file,$data+1);
}
else
{
$file=fopen($const.$id.$ext,'w+');
fwrite($file,1);
fclose($file);
}
?>
This will create files for each 'id' that you pass in the url.
Copy the above code in file for ex: counter.php and include in the file wherever the counter is required as :
<?php
include "counter.php"
?>
Hope this resolves your issue!
Each time a page is loaded, consider using parse_url() to determine the end query string. Enter that into your file.
$query = parse_url($url, PHP_URL_QUERY);
Try following code, which will work same way as your code except storing counter for individual id in individual file:
$fp = fopen("counter.txt".$_GET["id"], "r+");
while(!flock($fp, LOCK_EX)) { // acquire an exclusive lock
// waiting to lock the file }
$counter = intval(fread($fp, filesize("counter.txt".$_GET["id"]))); $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);
I think you want a separate counter for every page.
If yes, here is an edited version of your code:
$fp = fopen('counter'.intval($_GET['id']).'.txt', 'r+');
while(!flock($fp, LOCK_EX)) { // acquire an exclusive lock
// waiting to lock the file
}
$counter = intval(fread($fp, filesize('counter'.intval($_GET['id']).'.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);
I put the id into the filename.
Edit:
To make it in one counter.txt file(it is inefficient):
$fh = fopen('counter.txt', 'c+'); //Open file
while(!flock($fh, LOCK_EX)) {} //Lock file
$var = json_decode(fread($fh, filesize('counter.txt'))); //Json decode file to array
if(isset($var)) $var[(int)$_GET['id']]++; else $var[(int)$_GET['id']] = 1; //Increment the id's array value by one
ftruncate($fh, 0); //Erase file contents
fwrite($fh, json_encode($var)); //Write json encoded array
fflush($fh); //Flush to file
flock($fh, LOCK_UN); //Unlock file
fclose($fh); //Close file
But as said by h2ooooooo, a DB based solution will be better.(I will write it if you want)