first php projects
CLI Product Report
The project track starts with a command-line report because it keeps the first application boundary visible. The command receives arguments, validates them, calls ordinary PHP functions, prints a result, and exits deliberately when usage is wrong.
Build bin/products-report.php. It should print products for one allowed status: draft or published.
Keep Reporting Separate From The Terminal
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,
fn (array $product): bool => ($product['status'] ?? null) === $status,
));
}
The function does not know about $argv, terminal output, or exit codes. That makes it easy to reuse and test.
Add The CLI Boundary
The entry script reads arguments and translates failures into terminal output:
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/src/ProductReport.php';
$products = [
['name' => 'Notebook', 'status' => 'published'],
['name' => 'Desk lamp', 'status' => 'draft'],
];
$status = $argv[1] ?? '';
try {
foreach (productsWithStatus($products, $status) as $product) {
echo $product['name'] . PHP_EOL;
}
} catch (InvalidArgumentException $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(2);
}
Run:
php bin/products-report.php published
php bin/products-report.php archived
echo $?
The first command prints Notebook. The second writes an error to standard error and exits with code 2.
What This Project Teaches
CLI input is still external input. Validate it at the edge, keep reusable work outside the entry script, and document non-zero exits so schedulers and CI jobs can detect failure.
Practice
Practice: Build A Product Report Command
Create src/ProductReport.php and bin/products-report.php. Implement the guided command from the lesson, then add an optional csv output format.
Requirements
- Read a required status argument and an optional format argument.
- Validate against an allow-list such as
draftandpublished. - Print one product name per line by default.
- Print
name,statusrows when the format iscsv. - Return a non-zero exit code for invalid usage.
- Do not trust shell arguments merely because an operator supplied them.
- Avoid mixing business logic with
echostatements throughout the script. - Document exit codes for schedulers and CI.
Run the command with valid, missing, and invalid arguments. Record the observed output and exit code for each case.
Show solution
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/src/ProductReport.php';
$products = [
['name' => 'Notebook', 'status' => 'published'],
['name' => 'Desk lamp', 'status' => 'draft'],
];
$status = $argv[1] ?? '';
$format = $argv[2] ?? 'lines';
if (!in_array($format, ['lines', 'csv'], true)) {
fwrite(STDERR, "Format must be lines or csv.\n");
exit(2);
}
try {
foreach (productsWithStatus($products, $status) as $product) {
echo $format === 'csv'
? $product['name'] . ',' . $product['status'] . PHP_EOL
: $product['name'] . PHP_EOL;
}
} catch (InvalidArgumentException $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(2);
}
Run:
php bin/products-report.php published
php bin/products-report.php published csv
php bin/products-report.php archived
The normal result prints Notebook. CSV mode prints Notebook,published. The invalid status prints an error to standard error and exits with code 2.