CLI Product Report
bin/products-report.php
src/ProductReport.php
The command accepts a required draft or published status and an optional lines or csv format.
Start With A Pure Filter
Put the reusable rule in src/ProductReport.php:
<?php
declare(strict_types=1);
function productsWithStatus(array $products, string $status): array
{
if (!in_array($status, ['draft', 'published'], true)) {
throw new InvalidArgumentException('Status must be draft or published.');
}
return array_values(array_filter(
$products,
static fn (array $product): bool => ($product['status'] ?? null) === $status,
));
}
This function has no terminal dependency. A test can pass an array and compare the returned rows without replacing $argv, capturing output, or intercepting exit().
Read Arguments At The Boundary
In a CLI process, $argv[0] is the script name. User arguments start at $argv[1]:
<?php
declare(strict_types=1);
// no-execute: fragment requires CLI arguments.
$status = $argv[1] ?? null;
$format = $argv[2] ?? 'lines';
if ($status === null || !in_array($format, ['lines', 'csv'], true)) {
fwrite(STDERR, "Usage: php bin/products-report.php <draft|published> [lines|csv]\n");
exit(2);
}
Missing status and unknown format are usage errors. They write a diagnostic to STDERR, not into the report stream, and exit with 2. The filter separately rejects an unknown status using the same usage outcome.
Format One Row Correctly
Plain lines and CSV have different encoding rules. Isolate that choice:
<?php
declare(strict_types=1);
function writeProductRow(array $product, string $format): bool
{
if ($format === 'csv') {
return fputcsv(
STDOUT,
[(string) $product['name'], (string) $product['status']],
',',
'"',
'',
) !== false;
}
return fwrite(STDOUT, (string) $product['name'] . PHP_EOL) !== false;
}
Do not build CSV with string concatenation. fputcsv() quotes commas, quotes, and line breaks. PHP 8.5 also expects the escape argument to be supplied explicitly; the empty string selects standards-compatible escaping.
Compose The Command Last
The entry script now has a short job:
// Partial: follows argument parsing and the project require.
try {
$matches = productsWithStatus($products, $status);
} catch (InvalidArgumentException $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(2);
}
foreach ($matches as $product) {
if (!writeProductRow($product, $format)) {
fwrite(STDERR, "Could not write the report.\n");
exit(1);
}
}
Reaching the end gives the shell exit code 0. A valid filter with no matching rows is still successful; an output failure exits with 1; invalid usage exits with 2.
Run The Contract
php bin/products-report.php published
php bin/products-report.php published csv
php bin/products-report.php archived
echo $?
php bin/products-report.php
echo $?
Use a product named Desk, large to prove CSV mode still emits exactly two fields. Redirect standard output and standard error separately to verify diagnostics never corrupt report data.
Practice
Practice: Build A Product Report Command
Create src/ProductReport.php and bin/products-report.php, then extend the guided command into a report safe for people and automation.
Requirements
- Accept a required status and an optional
linesorcsvformat. - Allow only
draftandpublishedstatuses. - Keep filtering independent from
$argv, terminal output, andexit(). - Print one product name per line in
linesmode. - Produce valid two-column CSV with
fputcsv()incsvmode. - Pass the
fputcsv()escape argument explicitly for PHP 8.5. - Write report data to standard output and diagnostics to standard error.
- Exit with
0on success,2for invalid usage, and1if output fails. - Show a usage message for missing or invalid arguments.
Use a product name containing a comma or quote. Run valid, empty-result, missing-argument, invalid-status, and invalid-format cases. Record the output stream and exit code for each case.
Show solution
Keep productsWithStatus() and writeProductRow() in src/ProductReport.php. Build bin/products-report.php in three ordered sections.
Bootstrap And Parse
<?php
declare(strict_types=1);
// no-execute: first section of a CLI entry file; later sections complete it.
require dirname(__DIR__) . '/src/ProductReport.php';
const EXIT_FAILURE = 1;
const EXIT_USAGE = 2;
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "This command must run from the CLI.\n");
exit(EXIT_FAILURE);
}
$status = $argv[1] ?? null;
$format = $argv[2] ?? 'lines';
Reject Invalid Usage
Append this section to the same file:
// Partial: continues bin/products-report.php.
if ($status === null || !in_array($format, ['lines', 'csv'], true)) {
fwrite(STDERR, "Usage: php bin/products-report.php <draft|published> [lines|csv]\n");
exit(EXIT_USAGE);
}
$products = [
['name' => 'Notebook', 'status' => 'published'],
['name' => 'Desk, large', 'status' => 'published'],
['name' => 'Desk lamp', 'status' => 'draft'],
];
Filter And Write
Finish the file with the orchestration:
// Partial: final section of bin/products-report.php.
try {
$matches = productsWithStatus($products, $status);
} catch (InvalidArgumentException $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(EXIT_USAGE);
}
foreach ($matches as $product) {
if (!writeProductRow($product, $format)) {
fwrite(STDERR, "Could not write the report.\n");
exit(EXIT_FAILURE);
}
}
Run normal, CSV, missing-status, invalid-status, and invalid-format cases. CSV output for the comma-containing name is "Desk, large",published, and invalid commands write only to standard error with exit code 2.