Commit 9a530a1c5e8 for php.net

commit 9a530a1c5e88be9d17e00b0204d9e8614689d6ed
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Apr 23 12:55:15 2026 -0400

    Fix GH-21742: SplFileObject::fgets() throws at EOF in eof/fgets loop (#21853)

    The SplFileObject iterator-desync fix in 08dad097025 made
    spl_filesystem_file_read_ex throw "Cannot read from file" on the
    NULL-buffer path. SplFileObject::fgets() now throws inside the
    documented while (!$spl->eof()) $spl->fgets() idiom, because eof()
    returns false until a read attempt returns zero bytes.

    Keep the stricter semantics for next(), seek(), current(), fscanf().
    Narrow fgets() to silent=true and return empty string on FAILURE,
    restoring the PHP-8.5 contract.

    Fixes GH-21742

diff --git a/ext/spl/spl_directory.c b/ext/spl/spl_directory.c
index 1468cec6ccf..89af25dd9d3 100644
--- a/ext/spl/spl_directory.c
+++ b/ext/spl/spl_directory.c
@@ -2099,7 +2099,11 @@ PHP_METHOD(SplFileObject, fgets)
 		spl_filesystem_file_free_line(intern);
 		intern->u.file.current_line_num++;
 	} else {
-		if (spl_filesystem_file_read_ex(intern, /* silent */ false, /* line_add */ 1, /* csv */ false) == FAILURE) {
+		if (spl_filesystem_file_read_ex(intern, /* silent */ true, /* line_add */ 1, /* csv */ false) == FAILURE) {
+			if (php_stream_eof(intern->u.file.stream)) {
+				RETURN_EMPTY_STRING();
+			}
+			spl_filesystem_file_cannot_read(intern);
 			RETURN_THROWS();
 		}
 		RETVAL_STR_COPY(intern->u.file.current_line);
diff --git a/ext/spl/tests/SplFileObject/gh21742.phpt b/ext/spl/tests/SplFileObject/gh21742.phpt
new file mode 100644
index 00000000000..7ea60b53915
--- /dev/null
+++ b/ext/spl/tests/SplFileObject/gh21742.phpt
@@ -0,0 +1,35 @@
+--TEST--
+GH-21742 (SplFileObject::fgets() throws at EOF in while (!$spl->eof()) loop)
+--FILE--
+<?php
+$file = tempnam(sys_get_temp_dir(), 'spl');
+file_put_contents($file, "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\n");
+
+$spl = new SplFileObject($file, 'r');
+while (!$spl->eof()) {
+    echo $spl->fgets();
+}
+echo "clean exit\n";
+
+$empty = tempnam(sys_get_temp_dir(), 'spl');
+file_put_contents($empty, '');
+$spl2 = new SplFileObject($empty, 'r');
+$iter = 0;
+while (!$spl2->eof()) {
+    $iter++;
+    $spl2->fgets();
+    if ($iter > 3) break;
+}
+echo "empty-file iters=$iter\n";
+
+unlink($file);
+unlink($empty);
+?>
+--EXPECT--
+Line 0
+Line 1
+Line 2
+Line 3
+Line 4
+clean exit
+empty-file iters=1