Table of Contents
Back to Blog

PHP Deep Dive

How to Count Lines in a File Using PHP (Three Methods, Three Traps)

Count lines in a PHP file — file(), fgets loop, SplFileObject, and shell exec. Covers the feof off-by-one trap, OOM from file(), and the fastest method for large files with benchmarks.

PHP 7.4+PHP 8.3WordPress
Published: May 14, 2026Updated: May 14, 202613 min readAuthor: Line Counter Editorial Team
PHPWordPressFile I/OPerformanceCSV
Fatal error: Allowed memory size of 268435456 bytes exhausted
(tried to allocate 440 bytes) in C:\process_txt.php on line 109

That error came from a familiar PHP line-counting pattern:

$lines = count(file($path)) - 1;

The file was hundreds of MB. file() tried to read every line into a PHP array, and the process hit memory_limit.

The common fix is a fgets loop. That fixes the memory problem, but a widely copied version introduces a different bug: it increments after a failed read at EOF. If the file ends with a newline, it can count one extra line.

This guide gives you the practical answer for php count lines work:

  • use file() only for small known files
  • use a corrected fgets loop when readability matters
  • use fread plus substr_count($chunk, "\n") for php count lines large file workloads
  • use shell_exec('wc -l') only as an optional Unix fast path
  • use WordPress APIs when the file came from an upload

You will also see why count lines php snippets from old forum posts often disagree by one line, and why php file count lines memory failures show up first in CMS and upload workflows.

If you just need a count now, use the Line Counter tool. If you are writing PHP code, the details below are where the edge cases live.

Quick Method Guide

I want to...Use thisMain warning
Count a small filecount(file($path))loads the whole file
Stream a normal filefixed fgets loopdo not use while (!feof()) incorrectly
Count a large file fastfread chunks plus substr_countcount "\n", not PHP_EOL, for portable files
Use OOP styleSplFileObjectseek(PHP_INT_MAX) has edge cases
Use the fastest Linux pathwc -l via shell_execescape the path and handle missing trailing newline
Count a WordPress uploadWordPress-safe wrappercheck nonce, capability, file type, and file size

For most php count lines in file code, the production default is the chunked fread version. It is cross-platform, memory-stable, and fast enough for logs, CSV exports, and upload validation.

Method 1: file() - Simple but Dangerous for Large Files

The shortest PHP answer is:

<?php

$lineCount = count(file('data.txt'));

With basic error handling:

<?php

function countLinesSmallFile(string $filePath): int
{
    if (!file_exists($filePath)) {
        throw new RuntimeException("File not found: $filePath");
    }

    $lines = file($filePath, FILE_IGNORE_NEW_LINES);
    if ($lines === false) {
        throw new RuntimeException("Cannot read file: $filePath");
    }

    return count($lines);
}

The PHP manual describes file() as reading the entire file into an array. Each array element corresponds to one line. That makes it convenient, but it is the core php file count lines memory trap.

File sizeUse file()?Why
Under 5MByessimple and readable
5MB to 50MBmaybedepends on memory_limit and average line length
Over 50MBavoidPHP array overhead grows quickly
Hundreds of MBnolikely OOM in shared hosting or WordPress

The memory cost is more than the raw file size because PHP stores an array plus one string per line. A 100MB file can take far more than 100MB once split into array elements.

<?php

$before = memory_get_peak_usage(true);
$lines = file('large_file.txt');
$after = memory_get_peak_usage(true);

echo 'Peak delta: ' . (($after - $before) / 1024 / 1024) . " MB\n";

Use file() for small config files, small test fixtures, and scripts with known inputs. Do not use it as the default php count lines in file answer for user uploads.

That is the simplest rule for avoiding php file count lines memory incidents: if the file can be user-provided, generated by a job, or larger than you expected, do not turn it into a PHP array before counting.

Common flags:

<?php

// Remove line endings from each element.
$lines = file('data.txt', FILE_IGNORE_NEW_LINES);

// Skip empty lines.
$lines = file('data.txt', FILE_SKIP_EMPTY_LINES);

// Combine both flags.
$lines = file(
    'data.txt',
    FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
);

If your input is CSV, remember that raw line counting is not the same as row counting when quoted fields contain embedded newlines. Use fgetcsv() for record-aware CSV processing.

Method 2: fgets Loop - Streaming with an Off-by-One Trap

The streaming idea is right:

<?php

$handle = fopen('largefile.txt', 'r');
$lineCount = 0;

while (!feof($handle)) {
    fgets($handle);
    $lineCount++;
}

fclose($handle);

This is the classic fgets count lines php mistake. feof() does not mean "the next read will succeed." It reports EOF after the stream has reached EOF. The PHP manual's own fgets example increments only after fgets() returns a string.

Here is the fixed version:

<?php

function countLinesWithFgets(string $filePath): int
{
    if (!file_exists($filePath)) {
        throw new RuntimeException("File not found: $filePath");
    }

    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new RuntimeException("Cannot open file: $filePath");
    }

    $lineCount = 0;

    try {
        while (($line = fgets($handle)) !== false) {
            $lineCount++;
        }

        if (!feof($handle)) {
            throw new RuntimeException("Unexpected read error: $filePath");
        }
    } finally {
        fclose($handle);
    }

    return $lineCount;
}

This fixed fgets count lines php version handles:

  • empty files: returns 0
  • files with a trailing newline: no extra line
  • files without a trailing newline: counts the last line
  • large files: keeps only one line in memory

Use fgets when you need text-line semantics, validation per line, or simple code. For pure line counting, it still allocates one PHP string per line, so it is slower than chunk scanning.

The short version for fgets count lines php code is: feof() can help detect a read error after the loop, but fgets() must decide whether a line was actually read.

Method 3: fread Chunks - Best Pure PHP Large-File Default

When you only need a count, do not create one PHP string per line. Read fixed-size chunks and count newline bytes.

<?php

function countLinesFast(string $filePath): int
{
    $handle = fopen($filePath, 'rb');
    if ($handle === false) {
        throw new RuntimeException("Cannot open file: $filePath");
    }

    $lineCount = 0;
    $lastByte = '';
    $sawData = false;

    try {
        while (!feof($handle)) {
            $chunk = fread($handle, 65536);
            if ($chunk === false) {
                throw new RuntimeException("Cannot read file: $filePath");
            }
            if ($chunk === '') {
                continue;
            }

            $sawData = true;
            $lineCount += substr_count($chunk, "\n");
            $lastByte = $chunk[strlen($chunk) - 1];
        }
    } finally {
        fclose($handle);
    }

    if ($sawData && $lastByte !== "\n") {
        $lineCount++;
    }

    return $lineCount;
}

This is the best pure PHP answer to php count lines large file for most production code:

  • fixed memory usage
  • no array of lines
  • no one-string-per-line loop
  • correct empty-file handling
  • correct missing-trailing-newline handling

Why count "\n" instead of PHP_EOL?

For uploaded files, logs copied between machines, and files generated by other systems, count "\n".

$lineCount += substr_count($chunk, "\n");

Do not use this as your general-purpose counter:

$lineCount += substr_count($chunk, PHP_EOL);

PHP_EOL is the line ending of the server running PHP, not necessarily the line ending inside the file. A Windows CSV uploaded to a Linux PHP server contains "\r\n", and counting "\n" still returns the correct line-break count because each CRLF contains one LF.

This detail matters in count lines php code that runs in WordPress, shared hosting, CI, or Docker containers.

Method 4: SplFileObject - OOP Style with Quirks

SplFileObject gives PHP an object-oriented interface for files. A readable streaming version looks like this:

<?php

function countLinesSplReadable(string $filePath): int
{
    $file = new SplFileObject($filePath, 'r');
    $count = 0;

    while (!$file->eof()) {
        $line = $file->fgets();

        if ($line === '' && $file->eof()) {
            break;
        }

        $count++;
    }

    return $count;
}

There is also a famous SplFileObject count lines shortcut:

<?php

function countLinesSplSeek(string $filePath): int
{
    if (filesize($filePath) === 0) {
        return 0;
    }

    $file = new SplFileObject($filePath, 'r');
    $file->seek(PHP_INT_MAX);

    return $file->key() + 1;
}

The idea is:

  • SplFileObject::seek() moves to a zero-based line number
  • PHP_INT_MAX is larger than any real line number
  • key() returns the current zero-based line number
  • adding 1 turns the line number into a line count

This is compact, but not my default recommendation. It can be surprising on empty files and on files where a final newline produces a terminal empty line in the iterator model.

You may see examples that add flags:

<?php

$file = new SplFileObject($filePath, 'r');
$file->setFlags(
    SplFileObject::READ_AHEAD
    | SplFileObject::SKIP_EMPTY
    | SplFileObject::DROP_NEW_LINE
);

Those flags are useful when iterating and intentionally skipping empty records. The PHP manual notes that SKIP_EMPTY requires READ_AHEAD to work as expected. But this is not a general line-count fix: it can skip empty lines in the middle of the file, and key() semantics are harder to reason about once iterator flags are involved.

If blank lines are meaningful, use fgets or fread instead. SplFileObject count lines code is fine for OOP codebases, but the chunk scanner is clearer for raw counts.

Use SplFileObject count lines patterns when they match the rest of your codebase. Do not use the shortcut just because it is shorter; the edge cases are less obvious than the fixed stream loop.

Method 5: shell_exec wc -l php - Fastest on Unix, Risky by Default

On Linux and macOS, wc -l is written in C and is usually faster than any PHP loop:

<?php

function countLinesShell(string $filePath): int
{
    if (filesize($filePath) === 0) {
        return 0;
    }

    $escaped = escapeshellarg($filePath);
    $output = shell_exec("wc -l < $escaped");

    if ($output === null || $output === false) {
        throw new RuntimeException('shell_exec failed or is disabled');
    }

    $count = (int) trim($output);

    $handle = fopen($filePath, 'rb');
    if ($handle === false) {
        throw new RuntimeException("Cannot open file: $filePath");
    }

    try {
        fseek($handle, -1, SEEK_END);
        $lastByte = fread($handle, 1);
    } finally {
        fclose($handle);
    }

    return $lastByte === "\n" ? $count : $count + 1;
}

There are two caveats.

First, never concatenate a user-controlled path into a shell command:

<?php

// Dangerous.
$count = shell_exec("wc -l " . $_POST['filename']);

Use escapeshellarg() for the path:

<?php

$escaped = escapeshellarg($filePath);
$count = shell_exec("wc -l < $escaped");

Second, wc -l counts newline characters. A non-empty file without a final newline has one logical line more than the raw wc -l value. That is why the function above checks the last byte.

Check availability before using shell_exec wc -l php code:

<?php

function isShellExecAvailable(): bool
{
    if (!function_exists('shell_exec')) {
        return false;
    }

    $disabled = array_map(
        'trim',
        explode(',', (string) ini_get('disable_functions'))
    );

    return !in_array('shell_exec', $disabled, true);
}

For hardened servers and shared hosts, assume shell_exec may be disabled. For Windows, prefer the pure PHP fread method.

Benchmark: All Methods Compared

These benchmark figures are representative for PHP 8.3 on Linux with an SSD and a 500MB text file of roughly 5 million lines. They are directional, not a universal promise; average line length, storage, CPU cache, and PHP configuration all matter.

MethodTimePeak memorySecurityBest use
file() plus count()about 3.2sabout 900MBsafesmall files only
buggy fgets plus feofabout 4.8sabout 1MBsafeavoid because wrong
fixed fgets loopabout 4.8sabout 1MBsafereadable streaming
fread chunk scanabout 1.4sabout 64KBsafepure PHP large files
SplFileObjectabout 5.5sabout 1MBsafeOOP style
shell_exec('wc -l')about 0.3svery lowneeds escapingUnix fast path

The practical conclusion:

  • for php count lines in file examples under 10MB, file() is acceptable
  • for php count lines large file code, prefer fread chunk scanning
  • for Linux-only internal tools, shell_exec wc -l php can be fastest
  • for upload flows, pick safety and predictable memory before speed

Part 6: Counting Lines in WordPress Uploads

WordPress changes the problem. You are not just counting a file; you are handling a user-uploaded attachment inside a permissioned application.

This php count lines wordpress example resolves an attachment path, validates access, checks the file extension and MIME mapping, chooses a memory-safe path, and returns a WordPress-style result.

<?php

/**
 * Count lines in a WordPress uploaded TXT or CSV attachment.
 *
 * @return int|WP_Error
 */
function lc_count_uploaded_file_lines(int $attachmentId)
{
    if (!current_user_can('edit_post', $attachmentId)) {
        return new WP_Error(
            'permission_denied',
            __('You cannot access this attachment.', 'line-counter')
        );
    }

    $filePath = get_attached_file($attachmentId);
    if (!$filePath || !is_file($filePath)) {
        return new WP_Error(
            'file_not_found',
            __('Uploaded file not found.', 'line-counter')
        );
    }

    $allowedMimes = [
        'txt' => 'text/plain',
        'csv' => 'text/csv',
    ];
    $fileType = wp_check_filetype($filePath, $allowedMimes);

    if (!$fileType['type']) {
        return new WP_Error(
            'invalid_file_type',
            __('Only TXT and CSV files are supported.', 'line-counter')
        );
    }

    $fileSize = filesize($filePath);
    if ($fileSize === false) {
        return new WP_Error(
            'size_error',
            __('Cannot inspect uploaded file size.', 'line-counter')
        );
    }

    $memoryLimit = wp_convert_hr_to_bytes(ini_get('memory_limit'));
    $safeReadAllLimit = (int) ($memoryLimit * 0.25);

    if ($fileSize > 0 && $fileSize < min(10 * 1024 * 1024, $safeReadAllLimit)) {
        $lines = file($filePath, FILE_IGNORE_NEW_LINES);

        if ($lines === false) {
            return new WP_Error(
                'read_error',
                __('Cannot read uploaded file.', 'line-counter')
            );
        }

        return count($lines);
    }

    try {
        return LineCounter::count($filePath);
    } catch (RuntimeException $error) {
        return new WP_Error('read_error', $error->getMessage());
    }
}

add_action('wp_ajax_count_file_lines', function (): void {
    check_ajax_referer('count_lines_nonce', 'nonce');

    $attachmentId = absint($_POST['attachment_id'] ?? 0);
    if (!$attachmentId) {
        wp_send_json_error('Invalid attachment ID', 400);
    }

    $count = lc_count_uploaded_file_lines($attachmentId);

    if (is_wp_error($count)) {
        wp_send_json_error($count->get_error_message(), 400);
    }

    wp_send_json_success(['line_count' => $count]);
});

For stronger upload validation, use wp_check_filetype_and_ext() during the upload handling path. wp_check_filetype() is still useful here as a lightweight extension and MIME mapping check for an existing attachment.

This is the safe shape for php count lines wordpress plugins:

  • never trust a raw $_POST['path']
  • resolve the file with get_attached_file()
  • check current_user_can()
  • verify the nonce with check_ajax_referer()
  • avoid file() unless the file is small relative to memory_limit
  • fall back to a streaming counter

Part 7: A Production-Ready PHP Line Counter

This class chooses the simplest safe strategy:

  • small files: file() for clarity
  • large files on Unix with shell enabled: wc -l, only when not skipping empty lines
  • everything else: fread chunk scanning
  • skipEmpty: streaming with chunk carry-over, so lines split across chunks are handled correctly
<?php

final class LineCounter
{
    private const SMALL_FILE_BYTES = 10 * 1024 * 1024;
    private const CHUNK_BYTES = 65536;

    public static function count(
        string $filePath,
        bool $skipEmpty = false
    ): int {
        if (!is_file($filePath)) {
            throw new RuntimeException("File not found: $filePath");
        }

        $fileSize = filesize($filePath);
        if ($fileSize === false) {
            throw new RuntimeException("Cannot stat file: $filePath");
        }
        if ($fileSize === 0) {
            return 0;
        }

        if (!$skipEmpty && $fileSize < self::SMALL_FILE_BYTES) {
            $lines = file($filePath, FILE_IGNORE_NEW_LINES);
            if ($lines === false) {
                throw new RuntimeException("Cannot read file: $filePath");
            }

            return count($lines);
        }

        if (
            !$skipEmpty
            && PHP_OS_FAMILY !== 'Windows'
            && self::isShellExecAvailable()
        ) {
            return self::countWithShell($filePath);
        }

        return self::countWithStream($filePath, $skipEmpty);
    }

    private static function countWithShell(string $filePath): int
    {
        $escaped = escapeshellarg($filePath);
        $output = shell_exec("wc -l < $escaped");

        if ($output === null || $output === false) {
            throw new RuntimeException('shell_exec failed');
        }

        $count = (int) trim($output);

        $handle = fopen($filePath, 'rb');
        if ($handle === false) {
            throw new RuntimeException("Cannot open file: $filePath");
        }

        try {
            fseek($handle, -1, SEEK_END);
            $lastByte = fread($handle, 1);
        } finally {
            fclose($handle);
        }

        return $lastByte === "\n" ? $count : $count + 1;
    }

    private static function countWithStream(
        string $filePath,
        bool $skipEmpty
    ): int {
        $handle = fopen($filePath, 'rb');
        if ($handle === false) {
            throw new RuntimeException("Cannot open file: $filePath");
        }

        $count = 0;
        $lastByte = '';
        $sawData = false;
        $pending = '';

        try {
            while (!feof($handle)) {
                $chunk = fread($handle, self::CHUNK_BYTES);
                if ($chunk === false) {
                    throw new RuntimeException("Cannot read file: $filePath");
                }
                if ($chunk === '') {
                    continue;
                }

                $sawData = true;
                $lastByte = $chunk[strlen($chunk) - 1];

                if (!$skipEmpty) {
                    $count += substr_count($chunk, "\n");
                    continue;
                }

                $data = $pending . $chunk;
                $parts = explode("\n", $data);
                $pending = array_pop($parts);

                foreach ($parts as $line) {
                    if (trim($line) !== '') {
                        $count++;
                    }
                }
            }
        } finally {
            fclose($handle);
        }

        if ($skipEmpty) {
            return trim($pending) !== '' ? $count + 1 : $count;
        }

        return $sawData && $lastByte !== "\n" ? $count + 1 : $count;
    }

    private static function isShellExecAvailable(): bool
    {
        if (!function_exists('shell_exec')) {
            return false;
        }

        $disabled = array_map(
            'trim',
            explode(',', (string) ini_get('disable_functions'))
        );

        return !in_array('shell_exec', $disabled, true);
    }
}

Usage:

<?php

$total = LineCounter::count('data.txt');
$nonEmpty = LineCounter::count('config.txt', true);

PHP 8 named arguments also work:

<?php

$nonEmpty = LineCounter::count('config.txt', skipEmpty: true);

This class deliberately keeps shell usage out of the skipEmpty path. Counting only non-empty lines requires line awareness, so the stream path is safer than trying to build a portable shell pipeline.

Special Scenarios

Count uploaded CSV rows

If you need CSV rows rather than physical lines, use fgetcsv():

<?php

function countCsvRows(string $filePath): int
{
    $handle = fopen($filePath, 'rb');
    if ($handle === false) {
        throw new RuntimeException("Cannot open file: $filePath");
    }

    $rows = 0;

    try {
        while (($row = fgetcsv($handle)) !== false) {
            $rows++;
        }
    } finally {
        fclose($handle);
    }

    return $rows;
}

This matters because CSV fields can contain embedded newlines inside quoted values.

Count standard input

<?php

$count = 0;

while (($line = fgets(STDIN)) !== false) {
    $count++;
}

echo $count . PHP_EOL;

Count lines in a PHP string

<?php

function countLinesInString(string $text): int
{
    if ($text === '') {
        return 0;
    }

    $count = substr_count($text, "\n");

    return str_ends_with($text, "\n") ? $count : $count + 1;
}

If you need PHP 7.4 compatibility, replace str_ends_with($text, "\n") with substr($text, -1) === "\n".

Decision Tree

How large is the file?
|
+-- Under 10MB
|   +-- Use file() + count() if the input is trusted and bounded
|
+-- 10MB to 1GB
|   +-- Need each line? fixed fgets loop
|   +-- Need only the count? fread chunk scanning
|
+-- Over 1GB
|   +-- Unix internal server? wc -l with escapeshellarg and newline fix
|   +-- Shared host, Windows, WordPress, or user upload? fread chunk scanning

Do you need non-empty lines?
|
+-- Yes: stream with line carry-over, not raw substr_count only
+-- No: count "\n" bytes and fix the final unterminated line

PHP Version Compatibility

FeatureVersion supportNotes
file()PHP 4+Reads the whole file into an array
fgets() and fread()PHP 4+Safe streaming primitives
SplFileObjectPHP 5.1+Convenient OOP file wrapper
PHP_OS_FAMILYPHP 7.2+Used for Unix versus Windows branching
Typed parameters and return typesPHP 7+Used in the examples
str_ends_with()PHP 8.0+Replace with substr($text, -1) === "\n" on PHP 7.4
Named argumentsPHP 8.0+Optional; positional calls are shown first
shell_exec()PHP 4+Often disabled by hosting configuration

The article targets PHP 7.4+ codebases, with PHP 8.3 used for the benchmark environment. When a PHP 8-only convenience appears, the PHP 7.4-compatible alternative is shown nearby.

Production Checklist

  • Do not use file() for unknown or large files.
  • Do not use while (!feof($handle)) as the counting condition.
  • Increment only after fgets() returns a string.
  • Count "\n" rather than PHP_EOL for cross-platform uploaded files.
  • Add one for a non-empty file that does not end with "\n".
  • Guard empty files before fseek($handle, -1, SEEK_END).
  • Escape shell arguments with escapeshellarg() if you use shell_exec.
  • Check whether shell_exec is disabled.
  • In WordPress, use get_attached_file() and current_user_can().
  • For CSV records, use fgetcsv() instead of raw line counting.

Sources Checked

Processing a CSV upload in PHP?

Before you write the import logic, check the line count first. Paste the file into the Line Counter. No PHP runtime, no memory limits, no off-by-one surprises.

Frequently Asked Questions

How do I count lines in a file in PHP?

For small files, count(file($path)) is concise. For production and large files, use fread chunks and count '\n' bytes, or use a fixed fgets loop when line semantics matter.

Why does count(file($path)) cause memory errors?

PHP file() reads the entire file into an array, so each line becomes a string element plus array overhead. Large logs or CSVs can exceed memory_limit.

How do I fix the off-by-one error in PHP line counting?

Do not write while (!feof($handle)) { fgets($handle); $count++; }. Instead, increment only when fgets($handle) !== false.

How do I count lines in a large file in PHP?

Use fread with a 64KB or 1MB buffer, substr_count($chunk, '\n'), and add one if the non-empty file does not end with a newline.

How do I use SplFileObject to count lines?

SplFileObject can stream lines or use the seek(PHP_INT_MAX) trick, but guard empty files and trailing newline behavior. It is convenient, not the fastest production default.

Is shell_exec safe for counting lines?

Only if shell_exec is enabled, the platform has wc, and the file path is escaped with escapeshellarg. Never concatenate user input directly into a shell command.

How do I count lines in PHP without loading the file?

Use fgets, SplFileObject, or fread chunk scanning. All three stream from disk instead of building a full array.

How do I count lines in WordPress?

Use get_attached_file to resolve the upload path, check permissions and nonce, validate the file type, then use a streaming line counter for large uploads.

Related Guides