CLI Product Report
This first project turns ordinary PHP functions into a command-line program. Shell arguments are untrusted strings, standard output is for report data, standard error is for diagnostics, and the exit code tells automation whether the command succeeded.
bin/products-report.php
src/ProductReport.php
The command accepts a required draft or published status and an optional lines or csv format.
Keep Filtering Independent
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 knows nothing about $argv, terminal streams, CSV, or exit codes. A test can call it without simulating a terminal.
Define The CLI Contract
Put the boundary in bin/products-report.php:
<?php
declare(strict_types=1);
// no-execute: requires the project file created above and CLI arguments.
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';
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'],
];
try {
$matches = productsWithStatus($products, $status);
} catch (InvalidArgumentException $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(EXIT_USAGE);
}
foreach ($matches as $product) {
if ($format === 'csv') {
$written = fputcsv(
STDOUT,
[(string) $product['name'], (string) $product['status']],
',',
'"',
'',
);
if ($written === false) {
fwrite(STDERR, "Could not write the report.\n");
exit(EXIT_FAILURE);
}
continue;
}
echo $product['name'] . PHP_EOL;
}
Use fputcsv() instead of concatenating fields. It quotes commas, quotes, and line breaks correctly. Passing the escape argument explicitly avoids PHP 8.5's deprecation for relying on its default.
Exit code 0 means success, including a valid report with no rows. Code 2 means invalid usage, and 1 means an operational output failure.
Run Every Path
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 $?
CSV mode produces two valid fields even for Desk, large. Invalid or missing arguments write to standard error and exit with 2.
PHP Details To Notice
$argv[0]is the script name; user arguments begin at$argv[1].PHP_SAPIprevents accidental use through a web server.STDOUTandSTDERRkeep report data separate from diagnostics.PHP_EOLuses the platform line ending for plain text.exit()sets the status observed by a shell, CI job, or scheduler.
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
<?php
declare(strict_types=1);
// no-execute: requires the project file created in the lesson and CLI arguments.
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';
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'],
];
try {
$matches = productsWithStatus($products, $status);
} catch (InvalidArgumentException $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(EXIT_USAGE);
}
foreach ($matches as $product) {
if ($format === 'csv') {
if (fputcsv(STDOUT, [$product['name'], $product['status']], ',', '"', '') === false) {
fwrite(STDERR, "Could not write the report.\n");
exit(EXIT_FAILURE);
}
continue;
}
echo $product['name'] . PHP_EOL;
}
Run:
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 published json
echo $?
The comma-containing name becomes "Desk, large",published, so a CSV reader sees two fields. Invalid commands write only to standard error and exit with 2.