Phase 1 · PHP FoundationsModule 7~55 min read

Functions & a CLI Mini Project

Design reusable typed functions, work with scope and closures, and combine Phase 1 skills in an expense-summary project.

What you'll learn

Functions turn working statements into reusable, testable units. You'll design typed contracts, understand scope and callables, then finish Phase 1 with a complete expense-summary program.

  • Define focused functions with inputs and outputs
  • Use default, named, variadic, nullable, and union-typed arguments
  • Understand local scope and reference semantics
  • Pass closures and arrow functions as behavior
  • Decompose a mini project into a small pipeline

Define one clear responsibility

A function has a name, parameter list, optional return type, and body. A strong function performs one cohesive job and either returns a useful result or produces a deliberate side effect.

tax.php
<?php

declare(strict_types=1);

function calculateTax(float $subtotal, float $rate = 0.11): float
{
    if ($subtotal < 0 || $rate < 0) {
        throw new InvalidArgumentException('Values cannot be negative.');
    }

    return $subtotal * $rate;
}

echo number_format(calculateTax(80), 2);

Key idea

A type declaration documents and checks the function boundary. declare(strict_types=1) prevents scalar arguments from being silently coerced in calls made from that file.

Flexible arguments without vague contracts

arguments.php
<?php

declare(strict_types=1);

function formatPrice(
    float $amount,
    string $currency = 'USD',
    ?string $label = null,
): string {
    $price = $currency . ' ' . number_format($amount, 2);
    return $label === null ? $price : "{$label}: {$price}";
}

function average(float ...$values): float
{
    return array_sum($values) / count($values);
}

echo formatPrice(amount: 24.5, label: 'Total') . PHP_EOL;
echo average(8, 9.5, 10);
FeaturePurpose
$currency = 'USD'Optional parameter with a default
amount: 24.5Named argument for a readable call
?stringA string or null
int|floatOne of several declared types
float ...$valuesZero or more arguments collected into an array

Scope, values, and references

Variables created inside a function are local. Pass the information a function needs and return its result; reaching into global state makes testing and reuse harder.

scope.php
<?php

$taxRate = 0.11;

function priceWithTax(float $price, float $rate): float
{
    $tax = $price * $rate; // local
    return $price + $tax;
}

echo priceWithTax(50, $taxRate);

References are a special tool

PHP passes values by value using copy-on-write. A parameter such as &$value allows mutation of the caller's variable, but it hides an output. Prefer returning the new value unless in-place mutation is central to the API.

Closures and arrow functions

callables.php
<?php

$minimum = 10;

$isLarge = function (float $amount) use ($minimum): bool {
    return $amount >= $minimum;
};

$prices = [5, 12, 18];
$large = array_filter($prices, $isLarge);
$withTax = array_map(fn ($price) => $price * 1.11, $large);

echo implode(', ', $withTax);

A closure can capture outer values explicitly with use. An arrow function captures used outer values automatically by value and is ideal for one expression.

Phase project: expense summary

This program forms a small pipeline: validate the collection, calculate the overall total, group values by category, sort, and present the report. Each stage has a focused function.

expenses.php
<?php

declare(strict_types=1);

function validExpenses(array $expenses): array
{
    return array_filter(
        $expenses,
        fn (array $expense): bool =>
            isset($expense['category'], $expense['amount'])
            && is_numeric($expense['amount'])
            && (float) $expense['amount'] >= 0
    );
}

function totalExpenses(array $expenses): float
{
    return array_reduce(
        $expenses,
        fn (float $total, array $expense): float =>
            $total + (float) $expense['amount'],
        0.0
    );
}

function totalsByCategory(array $expenses): array
{
    $totals = [];

    foreach ($expenses as $expense) {
        $category = (string) $expense['category'];
        $totals[$category] = ($totals[$category] ?? 0.0)
            + (float) $expense['amount'];
    }

    arsort($totals);
    return $totals;
}

$expenses = validExpenses([
    ['category' => 'Food', 'amount' => 18.50],
    ['category' => 'Books', 'amount' => 32.00],
    ['category' => 'Food', 'amount' => 11.25],
    ['category' => 'Travel', 'amount' => 8.75],
]);

echo 'Total: $' . number_format(totalExpenses($expenses), 2) . PHP_EOL;

foreach (totalsByCategory($expenses) as $category => $amount) {
    echo $category . ': $' . number_format($amount, 2) . PHP_EOL;
}

Review the project like a professional

  • Correctness: try empty input, zero amounts, decimals, invalid rows, and repeated categories
  • Contracts: every function says what it accepts and returns
  • Separation: validation, calculation, grouping, and presentation are distinct
  • Readability: names communicate domain meaning; no unexplained temporary values
  • Extension: add a date field and report only one month without rewriting every function

Stretch challenge

Read expenses from a JSON file passed in $argv[1]. Reject a missing file cleanly, decode the JSON, then reuse the same calculation functions unchanged.

Recap & Phase 1 check

Key takeaways

  • Small, cohesive functions make code reusable and testable.
  • Parameter and return types turn assumptions into checked boundaries.
  • Pass dependencies as arguments and return results instead of relying on globals or hidden reference mutation.
  • Closures and arrow functions let array operations receive behavior.
  • A clean program is a pipeline of validated data, domain calculation, and presentation.

Quick check

1. What does strict_types change for scalar arguments?

2. How does an arrow function capture used outer variables?

3. Which design is easiest to test?

4. What is the first stage of the expense project pipeline?