Intermediate

I/O Operations in PHP

Learn how to handle console output, read from standard input, and work with files in PHP using practical Docker-ready examples

Input and output are how a program communicates with the outside world — printing results to a terminal, reading what a user types, and persisting data to files. PHP was born as a web language, where “output” usually means generating an HTML response, but its command-line interface (CLI) exposes a full set of stream and filesystem functions that feel familiar to anyone coming from C or Perl.

Because PHP is a dynamic, weakly-typed language, its I/O functions are pragmatic and forgiving: most return a useful value on success and false on failure, and PHP will happily coerce numbers and strings as needed. This tutorial moves beyond the echo you saw in Hello World to cover formatted output, reading from standard input, writing and reading files, and handling I/O errors gracefully.

By the end you’ll be able to build small command-line tools that prompt for input, process data, and read or write files — all runnable in a container with nothing installed locally.

Formatted Output

Beyond echo, PHP provides printf/sprintf for C-style formatted output and direct access to the standard output and error streams through the predefined constants STDOUT and STDERR.

Create a file named output.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
// echo prints one or more strings as-is
echo "Standard output line\n";

// printf writes a formatted string directly to output
printf("Name: %s, Age: %d\n", "Ada", 36);
printf("Pi is approximately %.2f\n", 3.14159);

// sprintf returns the formatted string instead of printing it
$padded = sprintf("%05d", 42);
echo $padded . "\n";

// Write explicitly to the standard output and standard error streams
fwrite(STDOUT, "Written to STDOUT\n");
fwrite(STDERR, "Written to STDERR\n");

The %s, %d, and %.2f placeholders map to string, integer, and fixed-precision float arguments. Writing to STDERR keeps diagnostic messages separate from real program output — important when a tool’s output is piped into another command.

Reading from Standard Input

The STDIN stream lets a CLI program read what the user types. fgets(STDIN) reads one line including the trailing newline, so wrapping it in trim() is the common idiom.

Create a file named input.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
// Prompt and read a line of text
echo "What is your name? ";
$name = trim(fgets(STDIN));

// Read a line and coerce it to an integer
echo "How old are you? ";
$age = (int) trim(fgets(STDIN));

printf("Hello, %s! Next year you will be %d.\n", $name, $age + 1);

When you pipe input into the program (as shown in the Docker section below), the prompts print without waiting, since the answers are supplied non-interactively.

Writing Files

PHP offers two levels of file writing: the one-shot file_put_contents() for when you have all the data at once, and the stream-based fopen/fwrite/fclose trio for incremental writes.

Create a file named write_file.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
$lines = [
    "First line",
    "Second line",
    "Third line",
];

// One-shot write: creates the file or overwrites it entirely
file_put_contents("notes.txt", implode("\n", $lines) . "\n");

// Append to an existing file with the FILE_APPEND flag
file_put_contents("notes.txt", "Appended line\n", FILE_APPEND);

// Stream-based writing gives finer control over the open file handle
$handle = fopen("log.txt", "w");
fwrite($handle, "Log entry 1\n");
fwrite($handle, "Log entry 2\n");
fclose($handle);

echo "Files written successfully\n";

The "w" mode truncates the file (or creates it); use "a" to append or "r" for read-only. Always call fclose() to flush buffered data and release the handle.

Reading Files

Reading mirrors writing. file_get_contents() slurps an entire file into a string, file() returns an array of lines, and fgets() in a loop reads one line at a time — the memory-efficient choice for large files.

Create a file named read_file.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// Create a sample file so this example is self-contained
file_put_contents("sample.txt", "apple\nbanana\ncherry\n");

// Read the whole file into one string
$contents = file_get_contents("sample.txt");
echo "Full contents:\n";
echo $contents;

// Read the file into an array of lines, dropping the newlines
$lines = file("sample.txt", FILE_IGNORE_NEW_LINES);
echo "Line count: " . count($lines) . "\n";

// Read line by line with a stream — ideal for large files
echo "Numbered lines:\n";
$handle = fopen("sample.txt", "r");
$number = 1;
while (($line = fgets($handle)) !== false) {
    echo $number . ": " . trim($line) . "\n";
    $number++;
}
fclose($handle);

The fgets() loop uses !== false (strict comparison) because a line containing only "0" would be falsy — a classic PHP gotcha that strict comparison avoids.

Handling I/O Errors

Filesystem operations can fail: a path may not exist, or permissions may be wrong. PHP’s older functions emit a warning and return false, while modern code often wraps failures in exceptions for structured handling.

Create a file named io_errors.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$path = "nonexistent/data.txt";

// The @ operator suppresses the built-in warning so we can handle it ourselves
$contents = @file_get_contents($path);
if ($contents === false) {
    fwrite(STDERR, "Error: could not read {$path}\n");
}

// Exceptions provide structured error handling around file operations
try {
    $handle = @fopen($path, "r");
    if ($handle === false) {
        throw new RuntimeException("Unable to open {$path}");
    }
    fclose($handle);
} catch (RuntimeException $e) {
    echo "Caught: " . $e->getMessage() . "\n";
}

echo "Program continues after handling the error\n";

Checking the return value is essential — because PHP is forgiving, an unchecked false can silently propagate through the rest of your program.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the official image
docker pull php:8.4-cli-alpine

# Formatted output
docker run --rm -v $(pwd):/app -w /app php:8.4-cli-alpine php output.php

# Reading input (pipe answers in via printf; -i keeps STDIN open)
printf "Ada\n36\n" | docker run --rm -i -v $(pwd):/app -w /app php:8.4-cli-alpine php input.php

# Writing files
docker run --rm -v $(pwd):/app -w /app php:8.4-cli-alpine php write_file.php

# Reading files
docker run --rm -v $(pwd):/app -w /app php:8.4-cli-alpine php read_file.php

# Handling I/O errors
docker run --rm -v $(pwd):/app -w /app php:8.4-cli-alpine php io_errors.php

Expected Output

Running output.php:

Standard output line
Name: Ada, Age: 36
Pi is approximately 3.14
00042
Written to STDOUT
Written to STDERR

Running input.php with the piped input Ada and 36:

What is your name? How old are you? Hello, Ada! Next year you will be 37.

Running write_file.php:

Files written successfully

Running read_file.php:

Full contents:
apple
banana
cherry
Line count: 3
Numbered lines:
1: apple
2: banana
3: cherry

Running io_errors.php:

Error: could not read nonexistent/data.txt
Caught: Unable to open nonexistent/data.txt
Program continues after handling the error

Key Concepts

  • echo vs printfecho outputs strings verbatim, while printf (and its string-returning sibling sprintf) apply C-style format specifiers like %s, %d, and %.2f.
  • Predefined stream constants — the CLI exposes STDIN, STDOUT, and STDERR as ready-to-use file handles; sending diagnostics to STDERR keeps them out of piped output.
  • Two ways to read and write — one-shot helpers (file_get_contents, file_put_contents, file) are concise, while the fopen/fread/fwrite/fclose stream API scales to large files and gives incremental control.
  • File modes matter"r" reads, "w" truncates-or-creates, and "a" appends; the FILE_APPEND flag adds to file_put_contents without a handle.
  • Check return values strictly — I/O functions return false on failure, so use === false / !== false to avoid PHP’s weak-typing traps (a line of "0" is falsy but valid data).
  • trim() on inputfgets(STDIN) keeps the trailing newline; trimming before use is the standard idiom for reading typed input.
  • Two error styles — legacy functions emit warnings and return false (suppress with @ and check manually), while modern code throws exceptions caught with try/catch for structured recovery.

Running Today

All examples can be run using Docker:

docker pull php:8.4-cli-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining