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 example
<?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 example
<?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 draft and published.
  • Print one product name per line by default.
  • Print name,status rows when the format is csv.
  • 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 echo statements 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 example
<?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.