<< Home

Chapter 23: CLI Scripting and Automation

PHP is not only for web applications. It can also serve as a powerful scripting tool for command line automation, background jobs, and system tasks. With the same syntax and runtime you use for web code, you can create command line utilities that integrate with APIs, process data, or monitor systems.

💡 CLI scripts benefit from the same good practices as web code: clear input validation, predictable exits, and proper logging for visibility.

Reading Arguments and Options

When you run a PHP script from the terminal, command line arguments are made available through the $argv array. You can also use getopt() for more structured parsing of options and flags.

Accessing positional arguments

The first element of $argv is always the script name. The remaining elements are the arguments provided by the user. You can read them directly or shift them to handle sequential inputs.

<?php
// example: php greet.php Robin
if ($argc < 2) {
  fwrite(STDERR, "Usage: php greet.php <name>\n");
  exit(1);
}

$name = $argv[1];
echo "Hello, $name\n";

Parsing options with getopt()

The getopt() function supports short options like -v or long options like --verbose. Each option can require a value, have an optional value, or be a simple flag.

<?php
// php script.php -f data.txt --verbose
$options = getopt('f:v', ['file:', 'verbose']);

if (isset($options['file'])) {
  echo "File: {$options['file']}\n";
}
if (isset($options['verbose']) || isset($options['v'])) {
  echo "Verbose mode enabled\n";
}
⚠️ Always provide a usage message for unknown or invalid options to make scripts friendlier and easier to automate.

Environment Variables and Config

Environment variables are key to portable scripts. They let you keep secrets and configuration out of code. You can read them with getenv() or from the $_ENV array, and tools like vlucas/phpdotenv can help load them from .env files.

Loading variables safely

<?php
// php dotenv example
require __DIR__ . '/vendor/autoload.php';

Dotenv\Dotenv::createImmutable(__DIR__)->load();

$dbHost = getenv('DB_HOST') ?: 'localhost';
$dbUser = $_ENV['DB_USER'] ?? 'root';

echo "Connecting to $dbHost as $dbUser\n";
💡 Keep sensitive values like API keys in environment variables rather than in arguments, since process listings may expose arguments to other users.

Interactive Scripts

Interactive CLI scripts prompt the user for input and provide feedback. You can use readline() for typed input and fgets(STDIN) for basic prompts. Formatting output clearly helps guide the user through a task.

Prompting for input and confirmation

<?php
$name = readline("Enter your name: ");
$confirm = readline("Are you sure (y/n)? ");

if (strtolower($confirm) === 'y') {
  echo "Welcome, $name!\n";
} else {
  echo "Cancelled.\n";
}

Adding colors and formatting

You can use ANSI escape codes to color output in terminals that support it. This helps highlight warnings or success messages.

<?php
echo "\033[32mSuccess!\033[0m\n"; // green
echo "\033[31mError!\033[0m\n";   // red
⚠️ Avoid colored output in cron jobs or when redirecting to files since escape codes can clutter logs.

Task Runners and Scheduling

CLI scripts can act as reusable tasks that you run periodically or chain together. You can integrate them with cron, systemd timers, or queue systems. Laravel’s Artisan and Symfony’s Console component both provide structured task management.

Using Laravel Artisan commands

// app/Console/Commands/CleanTemp.php
namespace App\Console\Commands;

use Illuminate\Console\Command;

class CleanTemp extends Command
{
  protected $signature = 'cleanup:temp';
  protected $description = 'Remove temporary files older than one day';

  public function handle(): int
  {
    $count = count(glob(storage_path('temp/*')));
    $this->info("Deleting $count temp files...");
    return Command::SUCCESS;
  }
}

Scheduling with Laravel’s scheduler

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
  $schedule->command('cleanup:temp')->dailyAt('01:00');
}
💡 Use cron only to trigger one scheduler entry point per project. Let the framework decide which tasks to run at each tick.

Packaging CLI Tools

You can distribute standalone CLI applications by packaging them as PHAR archives or Composer global packages. This makes installation and updates straightforward for other developers or servers.

Creating a PHAR archive

PHAR (PHP Archive) bundles your code and dependencies into a single executable file. You can create it manually or with box or phar-composer.

<?php
$phar = new Phar('tool.phar');
$phar->buildFromDirectory(__DIR__ . '/src');
$phar->setStub($phar->createDefaultStub('main.php'));

Installing with Composer globally

To share a command via Composer, define a bin entry in your composer.json. When installed globally, Composer will add a symlink in the user’s PATH.

{
  "name": "example/tool",
  "bin": ["bin/tool"]
}
⚠️ Keep global tools self-contained. Avoid reading project-specific config or dependencies unless the user explicitly opts in.

Long-running Workers

Long-running workers handle queues, background processing, and asynchronous tasks. PHP can maintain a process loop that polls for jobs or listens for messages, but you must watch memory leaks and handle signals properly to ensure stability.

Implementing a resilient job loop

<?php
declare(strict_types=1);

while (true) {
  $job = getNextJob(); // fetch from queue …
  if ($job) {
    try {
      handleJob($job);
    } catch (Throwable $e) {
      error_log('Job failed: ' . $e->getMessage());
    }
  }
  sleep(5); // adjust polling delay
}

Using system signals for graceful shutdown

Catch system signals like SIGTERM and SIGINT to stop workers cleanly during deployments or restarts. This prevents half-finished work and file corruption.

<?php
declare(ticks = 1);

$running = true;
pcntl_signal(SIGTERM, fn() => $GLOBALS['running'] = false);
pcntl_signal(SIGINT, fn() => $GLOBALS['running'] = false);

while ($running) {
  doWork();
  sleep(2);
}
echo "Worker stopped gracefully.\n";
💡 Monitor worker memory and restart when usage exceeds safe limits. Combine supervisors like systemd or supervisord with health checks for reliable uptime.