Skip to content

PHP CLI Scripts — Complete Guide to Command-Line PHP Applications

DodaTech Updated 2026-06-28 7 min read

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

  1. 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.
  2. 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.
  3. Ignoring stderr: Use fprintf(STDERR, ...) for error messages and diagnostic output. This keeps stdout clean for piped data.
  4. Forgetting to install PCNTL: Signal handling requires the pcntl extension. On many PHP installations, it must be explicitly enabled.
  5. 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

  1. 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.
  2. How do you read input from a pipe in PHP CLI?

    • Use fgets(STDIN) to read one line at a time, or stream_get_contents(STDIN) to read all piped input at once.
  3. 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.
  4. 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.
  5. 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

Can PHP be used for CLI applications?

Yes. PHP has a full CLI SAPI with stdin/stdout, signal handling, and argument parsing. Tools like Composer and PHPUnit are PHP CLI applications.

What is the shebang line for PHP CLI?

#!/usr/bin/env php allows executing the script directly with ./script.php without invoking php explicitly.

How do I make a PHP script run continuously?

Use an infinite loop with sleep intervals and signal handling. Install pcntl for proper signal dispatch with pcntl_signal_dispatch.

Can PHP CLI scripts handle concurrent requests?

PHP CLI is single-threaded. For concurrent processing, use pcntl_fork, the parallel extension, or queue workers that process one job at a time.

How do I pass arguments to a PHP CLI script?

Arguments are available in $argv. For structured parsing, use getopt or the Symfony Console component.

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