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
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
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
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:
0means success- non-zero means failure
<?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
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
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.
#!/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
STDERRwhen the name is missing - exits with
0on success and1on 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
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>withgetopt() - accepts an optional
--dry-runflag - rejects a missing or empty file option
- prints which file would be imported
- prints whether the command is in dry-run mode
- exits with
0on success and1on 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
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
0on success and1on 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
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.