php language basics

Project: CLI Receipt Calculator

A command-line receipt calculator is a good first project because it combines the PHP language features you have already learned without needing a browser, database, or framework. You work with arrays, strings, functions, type declarations, loops, arithmetic, and error handling in one small program.

The goal is not to build a full shop system. The goal is to practise turning simple input data into reliable output. That is the same shape as many junior PHP tasks: receive some data, validate it, calculate something, format it clearly, and make the failure cases predictable.

Start with the data

For this first version, keep the input data inside the script. That lets you concentrate on the PHP logic before adding command-line arguments or files later.

Each receipt line needs three pieces of information:

  • a product name
  • a quantity
  • a unit price

Use integer pennies or cents for prices. Avoid floats for money because decimal rounding can surprise you.

PHP example
<?php

declare(strict_types=1);

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => 'Pen', 'quantity' => 3, 'unitPrice' => 129],
    ['name' => 'Desk pad', 'quantity' => 1, 'unitPrice' => 1299],
];

This is a list of associative arrays. The outer array is the receipt. Each inner array is one line item.

Calculate line totals

A line total is the quantity multiplied by the unit price. Put that calculation in a function so the rule has one clear name.

PHP example
<?php

declare(strict_types=1);

function lineTotal(array $item): int
{
    return $item['quantity'] * $item['unitPrice'];
}

$item = ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499];

echo lineTotal($item);

// Prints:
// 998

The function returns an integer number of pennies. It does not format the value for display yet. Keeping calculation and display separate makes the code easier to test and reuse.

Format money at the edge

Humans do not want to read 998 when the value means 9.98. Add a small formatting function for output.

PHP example
<?php

declare(strict_types=1);

function formatMoney(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

echo formatMoney(998);

// Prints:
// GBP 9.98

The calculation still uses integers. Formatting only happens when printing the receipt.

Build the subtotal

A subtotal is the sum of all line totals. Use a loop because a receipt can contain any number of items.

PHP example
<?php

declare(strict_types=1);

function lineTotal(array $item): int
{
    return $item['quantity'] * $item['unitPrice'];
}

function subtotal(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        $total += lineTotal($item);
    }

    return $total;
}

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => 'Pen', 'quantity' => 3, 'unitPrice' => 129],
];

echo subtotal($items);

// Prints:
// 1385

This loop is deliberately simple. A junior developer should be comfortable reading and writing this style before reaching for more compact array helpers.

Now combine the calculation functions with output. The script can print one line per item, then the final total.

PHP example
<?php

declare(strict_types=1);

function lineTotal(array $item): int
{
    return $item['quantity'] * $item['unitPrice'];
}

function subtotal(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        $total += lineTotal($item);
    }

    return $total;
}

function formatMoney(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => 'Pen', 'quantity' => 3, 'unitPrice' => 129],
];

foreach ($items as $item) {
    echo $item['quantity'] . ' x ' . $item['name'];
    echo ' = ' . formatMoney(lineTotal($item)) . PHP_EOL;
}

echo 'Total: ' . formatMoney(subtotal($items)) . PHP_EOL;

// Prints:
// 2 x Notebook = GBP 9.98
// 3 x Pen = GBP 3.87
// Total: GBP 13.85

PHP_EOL prints the correct line ending for the current platform. In CLI scripts, it is clearer than embedding "\n" everywhere.

Add a discount rule

Business rules should be named. If orders of at least GBP 50 get GBP 5 off, put that rule in a function instead of hiding it inside a long output block.

PHP example
<?php

declare(strict_types=1);

function discountForSubtotal(int $subtotal): int
{
    if ($subtotal >= 5000) {
        return 500;
    }

    return 0;
}

$subtotal = 6200;
$discount = discountForSubtotal($subtotal);

echo $subtotal - $discount;

// Prints:
// 5700

The function accepts and returns pennies. That keeps the money rule consistent with the rest of the program.

Validate before calculating

Do not calculate with obviously bad data. A receipt item with an empty name, zero quantity, or negative price should be rejected near the boundary.

PHP example
<?php

declare(strict_types=1);

function validateItem(array $item): void
{
    if (trim($item['name'] ?? '') === '') {
        throw new InvalidArgumentException('Item name is required.');
    }

    if (($item['quantity'] ?? 0) < 1) {
        throw new InvalidArgumentException('Quantity must be at least 1.');
    }

    if (($item['unitPrice'] ?? -1) < 0) {
        throw new InvalidArgumentException('Unit price cannot be negative.');
    }
}

validateItem(['name' => 'Pen', 'quantity' => 3, 'unitPrice' => 129]);

echo 'Item accepted';

// Prints:
// Item accepted

Throwing an exception is useful here because bad input means the program should not continue as if the receipt is valid.

Handle failure for the CLI user

An exception message is useful to the developer, but a CLI program should still print a controlled message. Put the main work inside try and catch invalid input at the edge.

PHP example
<?php

declare(strict_types=1);

function validateItem(array $item): void
{
    if (trim($item['name'] ?? '') === '') {
        throw new InvalidArgumentException('Item name is required.');
    }
}

try {
    validateItem(['name' => '', 'quantity' => 1, 'unitPrice' => 100]);
    echo 'Receipt is valid' . PHP_EOL;
} catch (InvalidArgumentException $exception) {
    echo 'Cannot build receipt: ' . $exception->getMessage() . PHP_EOL;
}

// Prints:
// Cannot build receipt: Item name is required.

This is the same pattern you will use in larger applications: validate early, throw or return a clear failure, and keep the user-facing output controlled.

What a good solution includes

A good version of this project should have small named functions for the important rules. It should calculate money using integer pennies, format money only when printing, loop over all receipt lines, and reject invalid item data before totals are calculated.

It should also be easy to run from the terminal:

php receipt.php

Before moving on, make sure you can explain how the receipt data flows through validation, calculation, discounting, and output.

Practice

Task: Build A Receipt Calculator

Create a small receipt.php script that prints line totals and a final total for hard-coded receipt items.

Requirements

  • Use declare(strict_types=1);.
  • Store the receipt as an array of associative arrays.
  • Each item must have name, quantity, and unitPrice keys.
  • Use integer pennies for prices.
  • Create a lineTotal() function.
  • Create a subtotal() function.
  • Create a formatMoney() function.
  • Print one line per item and a final total.

Use this starting data:

PHP example
<?php

declare(strict_types=1);

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => 'Pen', 'quantity' => 3, 'unitPrice' => 129],
    ['name' => 'Desk pad', 'quantity' => 1, 'unitPrice' => 1299],
];

Expected output:

2 x Notebook = GBP 9.98
3 x Pen = GBP 3.87
1 x Desk pad = GBP 12.99
Total: GBP 26.84
Show solution
PHP example
<?php

declare(strict_types=1);

function lineTotal(array $item): int
{
    return $item['quantity'] * $item['unitPrice'];
}

function subtotal(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        $total += lineTotal($item);
    }

    return $total;
}

function formatMoney(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => 'Pen', 'quantity' => 3, 'unitPrice' => 129],
    ['name' => 'Desk pad', 'quantity' => 1, 'unitPrice' => 1299],
];

foreach ($items as $item) {
    echo $item['quantity'] . ' x ' . $item['name'];
    echo ' = ' . formatMoney(lineTotal($item)) . PHP_EOL;
}

echo 'Total: ' . formatMoney(subtotal($items)) . PHP_EOL;

// Prints:
// 2 x Notebook = GBP 9.98
// 3 x Pen = GBP 3.87
// 1 x Desk pad = GBP 12.99
// Total: GBP 26.84

The calculation functions work with integer pennies. Only formatMoney() turns those pennies into display text.

Task: Add Discount Rule

Extend the receipt calculator so orders of at least GBP 50 receive GBP 5 off.

Requirements

  • Keep prices as integer pennies.
  • Add a discountForSubtotal() function.
  • Return 500 when the subtotal is at least 5000.
  • Return 0 for smaller receipts.
  • Print the subtotal, discount, and final total.

Use this data so the discount applies:

PHP example
<?php

declare(strict_types=1);

$items = [
    ['name' => 'Keyboard', 'quantity' => 1, 'unitPrice' => 3499],
    ['name' => 'Mouse', 'quantity' => 1, 'unitPrice' => 1899],
];

Expected output:

1 x Keyboard = GBP 34.99
1 x Mouse = GBP 18.99
Subtotal: GBP 53.98
Discount: GBP 5.00
Total: GBP 48.98
Show solution
PHP example
<?php

declare(strict_types=1);

function lineTotal(array $item): int
{
    return $item['quantity'] * $item['unitPrice'];
}

function subtotal(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        $total += lineTotal($item);
    }

    return $total;
}

function discountForSubtotal(int $subtotal): int
{
    if ($subtotal >= 5000) {
        return 500;
    }

    return 0;
}

function formatMoney(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

$items = [
    ['name' => 'Keyboard', 'quantity' => 1, 'unitPrice' => 3499],
    ['name' => 'Mouse', 'quantity' => 1, 'unitPrice' => 1899],
];

foreach ($items as $item) {
    echo $item['quantity'] . ' x ' . $item['name'];
    echo ' = ' . formatMoney(lineTotal($item)) . PHP_EOL;
}

$subtotal = subtotal($items);
$discount = discountForSubtotal($subtotal);
$total = $subtotal - $discount;

echo 'Subtotal: ' . formatMoney($subtotal) . PHP_EOL;
echo 'Discount: ' . formatMoney($discount) . PHP_EOL;
echo 'Total: ' . formatMoney($total) . PHP_EOL;

// Prints:
// 1 x Keyboard = GBP 34.99
// 1 x Mouse = GBP 18.99
// Subtotal: GBP 53.98
// Discount: GBP 5.00
// Total: GBP 48.98

The discount rule is separate from the printing code, so it can be changed or tested without rewriting the receipt output.

Task: Reject Empty Item Name

Add validation to the receipt calculator so an item with an empty name is rejected before totals are calculated.

Requirements

  • Create a validateItem() function.
  • Throw InvalidArgumentException when the item name is empty after trimming.
  • Also reject quantities lower than 1.
  • Also reject negative unit prices.
  • Wrap the receipt-building code in try and catch.
  • Print a clear CLI error message when validation fails.

Use this data to test the failure path:

PHP example
<?php

declare(strict_types=1);

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => '   ', 'quantity' => 1, 'unitPrice' => 129],
];

Expected output:

Cannot build receipt: Item name is required.
Show solution
PHP example
<?php

declare(strict_types=1);

function validateItem(array $item): void
{
    if (trim($item['name'] ?? '') === '') {
        throw new InvalidArgumentException('Item name is required.');
    }

    if (($item['quantity'] ?? 0) < 1) {
        throw new InvalidArgumentException('Quantity must be at least 1.');
    }

    if (($item['unitPrice'] ?? -1) < 0) {
        throw new InvalidArgumentException('Unit price cannot be negative.');
    }
}

function lineTotal(array $item): int
{
    return $item['quantity'] * $item['unitPrice'];
}

function subtotal(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        validateItem($item);
        $total += lineTotal($item);
    }

    return $total;
}

function formatMoney(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

$items = [
    ['name' => 'Notebook', 'quantity' => 2, 'unitPrice' => 499],
    ['name' => '   ', 'quantity' => 1, 'unitPrice' => 129],
];

try {
    foreach ($items as $item) {
        validateItem($item);
    }

    foreach ($items as $item) {
        echo $item['quantity'] . ' x ' . $item['name'];
        echo ' = ' . formatMoney(lineTotal($item)) . PHP_EOL;
    }

    echo 'Total: ' . formatMoney(subtotal($items)) . PHP_EOL;
} catch (InvalidArgumentException $exception) {
    echo 'Cannot build receipt: ' . $exception->getMessage() . PHP_EOL;
}

// Prints:
// Cannot build receipt: Item name is required.

This version validates the full receipt before printing it. The catch block keeps the CLI output controlled when invalid input is found.