The command line is a boundary
A job invoked by a scheduler receives text, not intentions. Define accepted options, required values, and the policy for unexpected arguments before producing effects. With getopt, a valueless option can exist with value false; test key presence rather than value truthiness. Parsing ends at the first non-option argument, so putting a filename before --dry-run can prevent the option from being read. A program accepting named options only should reject remaining arguments. An environment variable containing text 0 is likewise not absent: getenv returns false when it cannot find the variable.
Separate failure, logging, and exit status
The scheduler chooses its next step from an exit-status contract. Logging an exception and ending with zero can let a dependent task proceed despite failure. Have the main function return a code and terminate the process after it completes cleanup. In the fixture executed on PHP 8.4.4, exit inside try terminates the process without running the associated finally. This is an observation of the tested runtime, not a claim of PHP 8.5 execution. finally also does not protect against machine failure. Resources surviving the process need their own recovery strategy and a way to identify ownership.
Temporary handlers need restoration
Some PHP warnings are not exceptions by default. A handler can convert selected classes into ErrorException, but that policy should be explicit and limited to the block needing it. Guard the boundary with try and finally and use restore_error_handler to restore the previous stack. Calling set_error_handler again with the old handler adds a stack entry instead of undoing installation. In a long-running worker, leaked state can affect the next task. Do not turn every warning into a retry: an input error or contract mismatch needs correction. Record the class and context without placing secrets in logs.
Retry limits and per-task state
Three total attempts mean the initial execution and at most two retries. Distinguish that limit from three additional retries. Classify eligible transient failures, define an overall deadline, and account for scheduler retries to avoid multiplying attempts across layers. Reset accumulators, parameters, and context per task in a long-running process. To measure local duration, use a monotonic clock difference, such as hrtime, and convert its declared units; the absolute value is not a calendar timestamp. A local flock coordinates participants respecting the same lock and filesystem guarantees. Do not present it as universal cross-machine election or as a substitute for idempotency.
Laboratory: one option and controlled exit
The program below accepts only --dry-run and performs no business writes. With that option, it should print dry-run followed by cleanup and exit with zero. Without the option, it returns code 2 after cleanup. With a positional operand before the option, it rejects invocation before starting work. The syntax subset is deliberately small: larger applications may use a complete parser with a documented contract. The test executes all three variants in separate processes because exit would terminate its caller. Observe stdout, stderr, and exit status together; none of these observations alone describes the entire outcome.
Summary for production handover
A runbook should state a valid invocation, exit-code meanings, attempt limits, and recovery signals. Include a dry-run example and a rejected input so the operator can confirm behavior before the window. Explain which local resources are released during controlled flow and which need reconciliation after interruption. If the job calls a database, connect scheduler state to durable outcomes without confusing missing acknowledgement with guaranteed failure. Relate this lesson to streams, resource ownership, and transactions. Production handover still requires testing the real runtime, extensions, permissions, and scheduler in addition to these local examples.
<?php
declare(strict_types=1);
function main(array $args): int {
// This laboratory deliberately accepts only this exact option syntax.
foreach (array_slice($args, 1) as $arg) {
if ($arg!== '--dry-run') {
return 2;
}
}
$options = getopt('', ['dry-run'], $rest);
if ($options === false || $rest < count($args)) {
return 2;
}
try {
if (!array_key_exists('dry-run', $options)) {
return 2;
}
echo 'dry-run'
return 0;
} finally {
echo '|cleanup'
}
}
exit(main($argv))An option placed after an invalid operand is rejected before work instead of being ignored and triggering a write.
Common pitfalls
Do not use value truthiness to test flag presence, hide failures with zero status, or terminate before controlled cleanup.
Related topics: PDO transactions and partial failures · JSON, contracts, and explicit failures · Streams, CSV, and controlled imports
A reliable job validates inputs, preserves per-task state, and reports an outcome consistent with its known effects.
Reference: PHP manual: function.getopt · PHP 8.5 reference; DR PHP 2026.3; new fixtures executed on PHP 8.4.4 / SQLite 3.51.2