PHP CLI Scripts — Complete Guide to Command-Line PHP Applications
In this tutorial, you will learn about PHP CLI Scripts. We cover key concepts, practical examples, and best practices to help you master this topic.
PHP CLI scripts are standalone PHP programs designed to be executed from the terminal, handling command-line arguments, standard input and output streams, system processes, and file system operations for automation tasks. This tutorial covers running PHP from the command line, processing arguments with $argv and getopt, reading and writing to stdin and stdout, building robust console commands with Symfony Console, handling signals, daemonizing scripts, and integrating CLI tools into cron jobs and CI pipelines.
What You'll Learn
- Running PHP scripts from the terminal with the CLI SAPI
- Parsing command-line arguments using $argv, $argc, and getopt
- Reading from stdin and writing to stdout and stderr
- Building structured console commands with Symfony Console
- Handling Process signals (SIGTERM, SIGINT) for graceful shutdown
- Using filesystem and process components for file and command operations
- Deploying CLI scripts as cron jobs and Docker entrypoints
Why It Matters
PHP is not just for web applications. Its CLI SAPI makes it an excellent choice for automation scripts, data processing pipelines, system administration tools, and long-running daemons. Tools like Composer (written in PHP), Laravel Artisan, and Symfony Console demonstrate that PHP can power robust command-line applications. Learning CLI scripting expands your PHP skills beyond the browser and makes you a more versatile developer.
Real-World Use
DodaZIP's file compression engine runs as a PHP CLI daemon that watches a directory for new files, compresses them, and uploads the results to cloud storage. The daemon handles hundreds of files per minute, signals for graceful shutdown during maintenance, and logs progress to stdout for monitoring with systemd.
Learning Path
flowchart LR A[Security] --> B[CLI Scripts\nYou are here] B --> C[Performance] style B fill:#f90,color:#fff
Running PHP from the Command Line
Create a simple script hello.php:
#!/usr/bin/env php
<?php
echo "Hello from PHP CLI!\n";
echo "PHP Version: " . PHP_VERSION . "\n";
Make it executable and run:
chmod +x hello.php
./hello.php
Output:
Hello from PHP CLI!
PHP Version: 8.3.0
You can also run it without the shebang:
php hello.php
Processing Command-Line Arguments
PHP provides $argv (array of arguments) and $argc (argument count):
#!/usr/bin/env php
<?php
if ($argc < 2) {
echo "Usage: ./greet.php <name>\n";
exit(1);
}
$name = $argv[1];
$greeting = $argv[2] ?? 'Hello';
echo "$greeting, $name!\n";
Usage and output:
$ ./greet.php Alice
Hello, Alice!
$ ./greet.php Bob "Good morning"
Good morning, Bob!
For more complex argument parsing, use getopt:
#!/usr/bin/env php
<?php
$options = getopt('o:', ['input:', 'output:', 'verbose', 'compress:']);
$input = $options['input'] ?? $options['i'] ?? null;
$output = $options['output'] ?? $options['o'] ?? null;
$verbose = isset($options['verbose']);
$compress = $options['compress'] ?? 'none';
if (!$input || !$output) {
echo "Usage: ./process.php --input <file> --output <file> [--verbose] [--compress gzip|zip]\n";
exit(1);
}
echo "Processing $input -> $output (compress: $compress, verbose: " . ($verbose ? 'yes' : 'no') . ")\n";
Usage:
$ ./process.php --input=data.csv --output=results.json --verbose --compress=gzip
Processing data.csv -> results.json (compress: gzip, verbose: yes)
Reading from stdin and Writing to stdout
Create a filter that reads lines, transforms them, and writes to stdout:
#!/usr/bin/env php
<?php
echo "Enter text (Ctrl+D to finish):\n";
while ($line = fgets(STDIN)) {
$line = trim($line);
if ($line === '') {
continue;
}
$reversed = strtoupper(strrev($line));
fprintf(STDOUT, "Reversed: %s\n", $reversed);
}
fprintf(STDERR, "Processing complete: %d lines handled\n", 0);
Usage with pipe:
echo -e "hello\nworld\nphp" | ./reverse.php
Output:
Reversed: OLLEH
Reversed: DLROW
Reversed: PHP
Building Commands with Symfony Console
For structured CLI applications, use the Symfony Console component:
composer require symfony/console
Create bin/app.php:
#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class GenerateReportCommand extends Command
{
protected function configure(): void
{
$this->setName('report:generate')
->setDescription('Generates a report from input data')
->addArgument('source', InputArgument::REQUIRED, 'Source file path')
->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format', 'json')
->addOption('verbose', 'v', InputOption::VALUE_NONE, 'Verbose output');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$source = $input->getArgument('source');
$format = $input->getOption('format');
if ($input->getOption('verbose')) {
$output->writeln("Reading data from: <info>$source</info>");
}
if (!file_exists($source)) {
$output->writeln("<error>File not found: $source</error>");
return Command::FAILURE;
}
$data = file_get_contents($source);
$output->writeln("Report generated in $format format. Size: " . strlen($data) . " bytes");
return Command::SUCCESS;
}
}
$app = new Application('Data Processor', '1.0.0');
$app->add(new GenerateReportCommand());
$app->run();
Usage:
$ php bin/app.php report:generate data.csv --format=csv -v
Reading data from: data.csv
Report generated in csv format. Size: 1024 bytes
Handling Signals
Long-running scripts should handle termination signals gracefully:
#!/usr/bin/env php
<?php
$running = true;
pcntl_signal(SIGTERM, function (int $signo) use (&$running) {
echo "\nReceived SIGTERM. Shutting down gracefully...\n";
$running = false;
});
pcntl_signal(SIGINT, function (int $signo) use (&$running) {
echo "\nReceived SIGINT (Ctrl+C). Shutting down gracefully...\n";
$running = false;
});
echo "Daemon started. PID: " . getmypid() . "\n";
echo "Send SIGTERM (kill " . getmypid() . ") or press Ctrl+C to stop.\n";
$counter = 0;
while ($running) {
echo "Working... ($counter)\n";
$counter++;
sleep(1);
pcntl_signal_dispatch();
}
echo "Cleanup complete. Exiting.\n";
Common Mistakes
- Not handling errors with exit codes: A script that always exits with 0 hides failures. Use exit(0) for success, exit(1) for errors, and higher codes for specific error types.
- Blocking on I/O without timeouts: Reading from a network socket or pipe without a timeout can hang the script forever. Use stream_set_timeout for non-blocking I/O.
- Ignoring stderr: Use fprintf(STDERR, ...) for error messages and diagnostic output. This keeps stdout clean for piped data.
- Forgetting to install PCNTL: Signal handling requires the pcntl extension. On many PHP installations, it must be explicitly enabled.
- Running CLI scripts without the correct working directory: Scripts run from cron may have a different CWD. Use DIR or chdir to set the working directory explicitly.
Practice Questions
What is the difference between $argc and $argv?
- $argc is an integer count of arguments including the script name. $argv is an array of arguments where $argv[0] is the script name.
How do you read input from a pipe in PHP CLI?
- Use
fgets(STDIN)to read one line at a time, orstream_get_contents(STDIN)to read all piped input at once.
- Use
What does exit code 1 mean in a CLI script?
- Exit code 0 indicates success; any non-zero code (typically 1) indicates an error. The calling process (shell, cron) can check this code.
How does getopt differ from $argv parsing?
- getopt supports option flags (-v, --verbose), required values (--output=file), and short/long option aliases. $argv is a raw positional array.
Challenge: Build a PHP CLI that recursively searches a directory for files matching a pattern, prints matching files with line counts, and accepts --ext, --min-size, and --verbose options.
Mini Project
Create a log analyzer CLI tool:
- Accept a log file path as the first argument.
- Support options: --level (filter by log level: ERROR, WARNING, INFO), --since (ISO date filter), --count-only (just show per-level counts), --output (export as JSON or CSV).
- Parse standard log format:
[2026-06-28 12:00:00] app.ERROR: Message {"context":"value"}. - Display a summary table with counts per level.
- Exit with code 1 if the file is not found or unreadable.
- Add verbose mode that shows each matched line.
FAQ
What's Next
Now that you can build CLI tools, learn how to optimize their performance with Performance.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro