start here

PHP CLI

PHP CLI is the command-line version of PHP. It is used for scripts, Composer, framework console commands, migrations, test runners, cron jobs, queue workers, imports, exports, and maintenance tools.

CLI code has different inputs and outputs from web PHP. Instead of $_GET, forms, cookies, and HTTP responses, CLI scripts deal with arguments, options, standard input, standard output, standard error, and exit codes.

Running PHP From The Terminal

Common CLI commands:

php -v
php -m
php --ini
php -l script.php
php script.php
php -r "echo PHP_VERSION . PHP_EOL;"

php -l is a syntax check. php -r runs a small piece of PHP without creating a file. php script.php runs a script file.

Inside CLI PHP, PHP_SAPI is normally cli.

PHP example
<?php

declare(strict_types=1);

echo 'SAPI: ' . PHP_SAPI . PHP_EOL;

// Prints:
// SAPI: cli

Positional Arguments With $argv

CLI arguments are available in $argv. The first item is the script name.

PHP example
<?php

declare(strict_types=1);

$name = $argv[1] ?? null;

if ($name === null || trim($name) === '') {
    fwrite(STDERR, "Usage: php greet.php <name>\n");
    exit(1);
}

echo 'Hello ' . trim($name) . PHP_EOL;

// Example:
// php greet.php Ada
//
// Prints:
// Hello Ada

This is the simplest way to accept input when a script has one or two required values.

Named Options With getopt()

Named options are clearer when a command has optional settings.

PHP example
<?php

declare(strict_types=1);

$options = getopt('', ['file:', 'dry-run']);
$file = $options['file'] ?? null;
$dryRun = array_key_exists('dry-run', $options);

if (!is_string($file) || $file === '') {
    fwrite(STDERR, "Usage: php import.php --file=orders.csv [--dry-run]\n");
    exit(1);
}

echo 'File: ' . $file . PHP_EOL;
echo 'Mode: ' . ($dryRun ? 'dry run' : 'write changes') . PHP_EOL;

// Example:
// php import.php --file=orders.csv --dry-run
//
// Prints:
// File: orders.csv
// Mode: dry run

The colon in file: means the option requires a value. dry-run has no colon because it is a boolean flag.

STDOUT, STDERR, And Exit Codes

Write normal output to STDOUT. Write errors, usage messages, and diagnostics to STDERR.

Exit codes tell the shell whether the command succeeded:

  • 0 means success
  • non-zero means failure
PHP example
<?php

declare(strict_types=1);

$path = $argv[1] ?? null;

if (!is_string($path) || !is_file($path)) {
    fwrite(STDERR, "File does not exist.\n");
    exit(1);
}

echo filesize($path) . PHP_EOL;
exit(0);

Exit codes matter in CI, cron, deploy scripts, and shell pipelines. A command that prints an error but exits with 0 can make automation think everything succeeded.

Reading From STDIN

STDIN lets a command receive piped input.

PHP example
<?php

declare(strict_types=1);

$input = stream_get_contents(STDIN);
$lines = array_filter(explode("\n", trim($input)));

echo 'Lines: ' . count($lines) . PHP_EOL;

// Example:
// printf "one\ntwo\n" | php count-lines.php
//
// Prints:
// Lines: 2

This is useful for small tools that work with output from other commands.

Environment Variables In CLI

CLI scripts often depend on environment variables. Cron and process managers may provide a different environment from your interactive terminal.

PHP example
<?php

declare(strict_types=1);

$appEnvironment = getenv('APP_ENV') ?: 'production';

echo 'Environment: ' . $appEnvironment . PHP_EOL;

// Example:
// APP_ENV=local php env.php
//
// Prints:
// Environment: local

If a command works manually but fails in cron, check PATH, working directory, PHP binary path, and environment variables.

Shebang Scripts

On Unix-like systems, a PHP script can be directly executable with a shebang line.

PHP example
#!/usr/bin/env php
<?php

declare(strict_types=1);

echo 'Hello from an executable PHP script' . PHP_EOL;

Then:

chmod +x hello
./hello

This is common for project tools in bin/.

What Makes A Good CLI Script

A good CLI script:

  • validates required arguments and options
  • prints a useful usage message
  • writes errors to STDERR
  • exits non-zero on failure
  • avoids hard-coded absolute paths where possible
  • handles missing files and empty input deliberately
  • is safe to run more than once when used for maintenance

For larger applications, framework console components usually provide better argument parsing, help text, commands, dependency injection, and test support. The raw PHP CLI basics still matter because those frameworks build on the same runtime concepts.

What You Should Be Able To Do

After this lesson, you should be able to run PHP from the terminal, read positional arguments, parse named options, read STDIN, write errors to STDERR, and return meaningful exit codes.

For junior PHP work, this matters because CLI tasks are part of real projects: imports, migrations, scheduled jobs, one-off fixes, test commands, and deployment checks all rely on command-line PHP.

Practice

Practice: Build A Positional CLI Command

Create a small CLI script that greets a person by name.

Task

Build a script that:

  • reads the first positional argument from $argv
  • trims the name
  • prints Hello <name> when a name is provided
  • prints a usage message to STDERR when the name is missing
  • exits with 0 on success and 1 on failure

Use strict types. Keep example commands and expected output inside the PHP code block as comments.

Afterward, add a short note explaining why errors should go to STDERR.

Show solution

This solution validates the required argument before doing the command's normal work.

PHP example
<?php

declare(strict_types=1);

$name = $argv[1] ?? null;
$name = is_string($name) ? trim($name) : '';

if ($name === '') {
    fwrite(STDERR, "Usage: php greet.php <name>\n");
    exit(1);
}

echo 'Hello ' . $name . PHP_EOL;
exit(0);

// Example:
// php greet.php Ada
//
// Prints:
// Hello Ada
//
// Failure example:
// php greet.php
//
// STDERR:
// Usage: php greet.php <name>

Errors should go to STDERR so normal output can still be piped to another command or file without mixing it with diagnostic messages.

Task: Named CLI Option

Create a small import command that uses named CLI options.

Requirements

Build a script that:

  • reads --file=<path> with getopt()
  • accepts an optional --dry-run flag
  • rejects a missing or empty file option
  • prints which file would be imported
  • prints whether the command is in dry-run mode
  • exits with 0 on success and 1 on failure

Use strict types. Keep example commands and expected output inside the PHP code block as comments.

Afterward, add a short note explaining why named options are clearer than positional arguments for this command.

Show solution

This solution uses getopt() for a required value option and an optional boolean flag.

PHP example
<?php

declare(strict_types=1);

$options = getopt('', ['file:', 'dry-run']);
$file = $options['file'] ?? null;
$dryRun = array_key_exists('dry-run', $options);

if (!is_string($file) || trim($file) === '') {
    fwrite(STDERR, "Usage: php import.php --file=orders.csv [--dry-run]\n");
    exit(1);
}

echo 'Import file: ' . trim($file) . PHP_EOL;
echo 'Mode: ' . ($dryRun ? 'dry run' : 'write changes') . PHP_EOL;
exit(0);

// Example:
// php import.php --file=orders.csv --dry-run
//
// Prints:
// Import file: orders.csv
// Mode: dry run

Named options are clearer than positional arguments here because the command has more than one setting. --file=orders.csv --dry-run explains itself better than relying on argument order.

Task: Stdin And Exit Code

Create a small CLI script that reads lines from STDIN and reports how many non-empty lines it received.

Requirements

Build a script that:

  • reads from STDIN
  • treats empty input as an error
  • writes the line count to normal output
  • writes the error message to STDERR
  • exits with 0 on success and 1 on failure

Use strict types. Keep example commands and expected output inside the PHP code block as comments.

Afterward, add a short note explaining why exit codes matter in automation.

Show solution

This solution supports shell pipelines and reports failure with a non-zero exit code.

PHP example
<?php

declare(strict_types=1);

$input = stream_get_contents(STDIN);
$lines = array_values(array_filter(
    explode("\n", trim($input)),
    static fn (string $line): bool => trim($line) !== '',
));

if ($lines === []) {
    fwrite(STDERR, "No input received.\n");
    exit(1);
}

echo 'Non-empty lines: ' . count($lines) . PHP_EOL;
exit(0);

// Example:
// printf "one\ntwo\n\n" | php count-lines.php
//
// Prints:
// Non-empty lines: 2
//
// Failure example:
// printf "" | php count-lines.php
//
// STDERR:
// No input received.

Exit codes matter because CI jobs, cron tasks, deployment scripts, and shell pipelines use them to decide whether a command succeeded. A failed command should not exit with 0.