PHP CLI & Command-Line Scripts
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list phpstan and symfony-console, but neither will automatically catch missing SIGTERM handlers, improper exit codes, or memory leaks in long-running loops without careful custom configuration. The code_pattern field notes 'automated: no', confirming these issues surface only under review or when the script is actually run in production-like conditions.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix spans several concerns: adding proper IO handling, exit codes, explicit memory/time limits, and pcntl_signal() handlers. While each fix individually may be small, correcting a CLI script that misuses HTTP superglobals, leaks memory in long loops, and lacks graceful shutdown handling requires touching multiple parts of the script and its surrounding infrastructure (cron config, CI pipeline expectations), landing this at e5.
Closest to 'persistent productivity tax' (b5). The applies_to contexts are cli and queue-worker — not the entire codebase — but long-running queue workers are often load-bearing components. Misconfigurations (missing limits, no SIGTERM handling, wrong exit codes) create ongoing maintenance overhead every time a new CLI script or worker is added, making this a persistent tax on those work streams rather than a one-off fix.
Closest to 'serious trap' (t7). The misconception is explicit: developers assume PHP CLI shares the same php.ini as the web server, but it uses a separate configuration with different defaults for memory_limit, max_execution_time, and extensions. Additionally, CLI does not reset state between iterations, contradicting the web-request mental model most PHP developers carry. This directly contradicts how the web PHP process works, qualifying as a t7 trap that contradicts a familiar analogous concept.
Also Known As
TL;DR
Explanation
PHP CLI runs without a web server, with no request timeout, unlimited execution time by default, and access to stdin/stdout/stderr via STDIN, STDOUT, and STDERR constants. Arguments are available via $argv/$argc. Key differences from web PHP: no $_GET/$_POST/$_SERVER HTTP keys, different error output (stderr), and no output buffering by default. Use PHP CLI for: queue workers (long-running), scheduled tasks (cron), database migrations, and build scripts. Symfony Console and Laravel Artisan provide command frameworks with argument parsing, progress bars, and table output. Always handle signals (pcntl_signal) in long-running workers for graceful shutdown. Long-running CLI processes must actively manage memory — objects and references persist across iterations unlike web requests.
Common Misconception
Why It Matters
Common Mistakes
- Not setting the correct shebang (#!/usr/bin/env php) for executable PHP scripts.
- Using $_SERVER['REQUEST_URI'] or other HTTP superglobals in CLI scripts — they are not populated.
- Not handling exit codes — a CLI script that exits with 0 on error confuses CI pipelines.
- Ignoring that CLI has no memory or time limits by default — add them explicitly for long-running tasks.
- Running long loops without freeing memory (e.g. ORM entities, large arrays) — leads to memory leaks and crashes.
Code Examples
// CLI script with no exit code handling — CI can't detect failure:
<?php
try {
runImport();
} catch (Exception $e) {
echo $e->getMessage(); // Prints error but exits with 0 — CI thinks success
}
// Fix: exit(1) on failure
#!/usr/bin/env php
<?php
// Guard: must not run via web
if (php_sapi_name() !== 'cli') {
fwrite(STDERR, "CLI only.\n"); exit(1);
}
// Parse arguments
$opts = getopt('u:v', ['user:', 'verbose']);
$userId = $opts['u'] ?? $opts['user'] ?? null;
$verbose = isset($opts['v']) || isset($opts['verbose']);
if (!$userId) {
fwrite(STDERR, "Usage: php script.php -u <user_id>\n"); exit(1);
}
fwrite(STDOUT, "Processing user $userId\n");
// Exit codes: 0 = success, non-zero = error
exit(0);