fgetc
Reads and returns a single byte from an open file pointer, or false at end of file.
fgetc(resource $stream): string|false- Added in PHP 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$stream | resource | A valid file pointer opened by fopen() or fsockopen() and not yet closed by fclose(). |
Return value
string|falseA string containing a single character read from the stream, or false on EOF.
Errors
Throws a TypeError when the argument is not a valid open stream resource.
fgetc() advances the file pointer by exactly one byte and returns it as a one-character string. Despite the name, it knows nothing about character encodings — a UTF-8 é arrives as two separate calls returning two meaningless bytes.
When to reach for it
Hand-written parsers and tokenizers, where you need to look at one byte, decide, and sometimes push back with fseek($h, -1, SEEK_CUR). Scanning for a delimiter that fgets() cannot express. Reading a binary header field by field where the field widths depend on what you have already read.
For anything line-shaped use fgets(), and for fixed-size blocks use fread(). Both do far fewer round trips through the stream layer. fgetc() reads from the same buffer, so it is not doing a syscall per byte, but the per-call function overhead still makes a byte loop over a large file noticeably slower than fread() plus str_split().
What trips people up
The return value is a string, and two possible bytes are falsy: "0" and — from binary data — nothing else, since "\0" is truthy in PHP 8. So while ($c = fgetc($h)) silently stops at the first zero digit in the file. Compare with !== false, always.
Example
When to use it
- Tokenise a small expression or template language one byte at a time with one-byte lookahead
- Scan forward to a delimiter that fgets() cannot express, such as a semicolon
- Walk a binary header whose next field width depends on the byte just read
- Count occurrences of a control character without loading the whole file into memory
Frequently asked questions
Why does my fgetc loop stop early?
You almost certainly wrote while ($c = fgetc($h)), and the byte "0" is falsy in PHP so the loop ends at the first zero digit. Use while (($c = fgetc($h)) !== false).
Does fgetc read one character or one byte?
One byte. Multibyte characters such as UTF-8 accented letters span several bytes and come back split across several calls, so fgetc() is only safe for ASCII or binary data.
Is fgetc slower than fread?
Yes, meaningfully so on large files — not because of syscalls, since the stream is buffered, but because of the per-call overhead of millions of function calls. Read a block with fread() and iterate over it in PHP when throughput matters.
More examples
The falsy-zero trap
The loop reads "1", then reads "0", which PHP evaluates as false, and stops — so it reports 1 of 5 bytes. The file is not corrupt and no warning is raised; the data just silently disappears. This is the single most common fgetc() bug.
<?php
file_put_contents("digits.txt", "10203");
$h = fopen("digits.txt", "r");
$n = 0;
while ($c = fgetc($h)) { // wrong: "0" is falsy
$n++;
}
fclose($h);
echo "read $n of 5 bytes\n";
unlink("digits.txt");Bytes, not characters
The file holds five characters but six bytes: é is encoded as c3 a9 in UTF-8, and fgetc() hands back c3 and a9 as two separate one-byte strings. Neither is a valid character on its own, so never build user-facing text by concatenating fgetc() results from a multibyte file.
<?php
file_put_contents("utf8.txt", "héllo");
$h = fopen("utf8.txt", "r");
$hex = [];
while (($c = fgetc($h)) !== false) {
$hex[] = bin2hex($c);
}
fclose($h);
echo count($hex), " bytes: ", implode(" ", $hex), "\n";
echo "characters: ", count(preg_split('//u', "héllo", -1, PREG_SPLIT_NO_EMPTY)), "\n";
unlink("utf8.txt");One-byte lookahead with pushback
This is the pattern fgetc() exists for: read one byte, decide, and unread it with fseek() when it belongs to the next token. The result is [12, "+", 345] — integers for the numeric runs and the operator kept as a string.
<?php
file_put_contents("expr.txt", "12+345");
$h = fopen("expr.txt", "r");
$tokens = [];
while (($c = fgetc($h)) !== false) {
if (ctype_digit($c)) {
$num = $c;
while (($d = fgetc($h)) !== false && ctype_digit($d)) {
$num .= $d;
}
if ($d !== false) {
fseek($h, -1, SEEK_CUR); // push the non-digit back
}
$tokens[] = (int) $num;
} else {
$tokens[] = $c;
}
}
fclose($h);
print_r($tokens);
unlink("expr.txt");
Discussion