If the file you're reading were a "simple" file with nothing but line breaks as delimiters, then reading it backwards would be fairly simple. However, CSV is a more complex format with field and row delimiters, enclosures [quotes], and escapes. You could encounter data such as:
id,name,value
1,foo,"hello world"
2,bar,"hello
world"
3, baz,"""hello
world"""
Which is perfectly valid CSV, but would break most solutions currently proposed in this thread to read the data backwards, as well as this thread which this question was briefly marked as duplicate of.
The most reliable way to do this would be to read the file forwards first, and then use that information to read it in reverse. The simple version is to just stuff everything into an array and then read that backwards, eg:
$f = fopen("./data/data-esp8266-$idluf-$currentdata.csv", "r");
fgets($f);
$lines = [];
while (($lines[] = fgetcsv($f)) !== false) {}
for( $i=count($lines)-1; $i>=0; --$i ) {
$line = lines[$i];
$row = $line[0]; // Dobbiamo ottenere la riga effettiva (è il primo elemento in un array di 1 elemento)
$cells = explode(";",$row);
echo "<tr>\n";
foreach ($cells as $cell) {
echo "<td><a style='text-decoration:none;color:#fff;' class='tooltip' data-tool=' $cell'>" . htmlspecialchars($cell) . "</a></td>\n";
}
echo "</tr>\n";
}
fclose($f);
but if you're processing a large file you may run into memory constraints trying to store all that data.
An alternative would be to read the file forward once first, but only store the offsets in the file for the beginning of the records, and then use those offsets to iterate again in reverse.
function csv_reverse($handle, ...$csv_options) {
$offsets = [];
do {
$offsets[] = ftell($handle);
} while($row = fgetcsv($handle, ...$csv_options));
array_pop($offsets); // last offset is EOF
for( $i=count($offsets)-1; $i>=0; --$i ) {
fseek($handle, $offsets[$i]);
yield fgetcsv($handle, ...$csv_options);
}
}
$f = fopen("./data/data-esp8266-$idluf-$currentdata.csv", "r");
fgets($f); // assuming that this discards the header row
$lines = [];
while (($lines[] = fgetcsv($f)) !== false) {}
foreach( csv_reverse($f) as $line ) {
// same as above
}
fclose($f);
Live example: https://3v4l.org/0sc8q
There is a tradeoff here, in that the file must be traversed twice, but that's going to have to be a necessary evil if there is a memory constraint.
All this said, the better option would be to have this data in a database, if possible, which can trivially re-order the data for you on the fly. This code is already kinda-sorta reimplementing DB-related functionality, but worse.
Edit, because @Your-Common-Sense won't stop bothering me about this.
Here is a first-pass buffered reverse CSV iterator that respects quotes, and escaped quotes [probably] but probably has other less-common edge-case issues.
define('DEBUG', false);
function chunk_read_reverse($handle, $bufsz=4096) {
fseek($handle, 0, SEEK_END);
do{
$cur = ftell($handle);
if( $cur - $bufsz <= 0 ) {
if( $cur == 0 ) { return; }
fseek($handle, 0);
yield fread($handle, $cur);
return;
} else {
fseek($handle, -1 * $bufsz, SEEK_CUR);
}
$buffer = fread($handle, $bufsz);
yield $buffer;
fseek($handle, -1 * $bufsz, SEEK_CUR);
} while(true);
}
function csv_reverse_iterate($handle, ...$csv_options) {
$quote = $csv_options[1] ?? '"';
$endl = "\n";
$buffer = '';
$end = 0;
$q_count = 0;
foreach(chunk_read_reverse($handle) as $chunk) {
$buffer = $chunk . $buffer;
$buflen = strlen($buffer);
if(DEBUG){printf("bufflen: %4d, end: %4d\n", $buflen, $end);}
for( $i=strlen($chunk)-1; $i>=0; --$i ) {
$c = $buffer[$i];
if(DEBUG){printf("i: %3d, e: %3d, c: %1s, x: %s, q: %d\n", $i, $end, trim($c), bin2hex($c), $q_count);}
if( $c === $quote ) {
++$q_count;
}
else if( $c === $endl && $q_count % 2 == 0 ) {
yield str_getcsv(substr($buffer, $i+1, ($buflen-$end)-($i+1)), ...$csv_options);
$end = $buflen - $i;
}
}
$buffer = substr($buffer, 0, $buflen-$end);
$end = 0;
}
yield str_getcsv($buffer, ...$csv_options);
}
$csv = <<<_E_
id,name,value
1,foo,"hello world"
2,bar,"hello
world"
3, baz,"""hello
world"""
_E_;
$in = fopen('php://memory', 'rwb');
fwrite($in, $csv);
rewind($in);
foreach( csv_reverse_iterate($in) as $row ) {
var_dump($row);
}
I don't necessarily recommend using this, as it only cares about quotes and line breaks and does not account for other potential format quirks. But if you want to be pedantic like @Your-Common-Sense and save a questionably-significant amount of processing time, then you can use this slightly-worse-but-maybe-slightly-faster code.
Let me reiterate: If you need to do this enough that a few extra milliseconds makes a difference, then you should almost certainly be using a database, or database-like solution, and not CSV/flat files. This code was written purely out of spite.