Skip to main content

🧩 Lesson 7: Functions

Functions are the building blocks of organized code. Instead of writing the same logic over and over, you wrap it in a function and call it by name — like creating your own custom PHP commands.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Declare and call functions with parameters and return values
  • Use default parameter values and named arguments
  • Add type hints and return types for safer, self-documenting code
  • Understand variable scope and the difference between local and global
  • Create anonymous functions (closures) and arrow functions
  • Pass functions as arguments using callbacks

Estimated Time: 50 minutes

Prerequisites: Lesson 6 (loops, break/continue)

📑 In This Lesson

Why Functions?

📖 Definition

Function: A named, reusable block of code that performs a specific task. Functions can accept input (parameters), process it, and return output (a return value).

Functions solve three fundamental problems:

Problem Without Functions With Functions
Repetition Copy-paste the same code everywhere Write once, call many times
Complexity One massive file, hard to follow Small, focused pieces with clear names
Maintenance Bug fix? Update every copy Fix it in one place
flowchart LR A["Input
(Parameters)"] --> B["Function
Process Data"] B --> C["Output
(Return Value)"]

You've already used plenty of built-in functions: echo, strlen(), var_dump(), number_format(), count(), in_array(). Now you'll learn to create your own.

Declaring and Calling Functions

Basic Syntax

<?php
// Declare (define) a function
function greet() {
    echo "Hello, World! 👋\n";
}

// Call (invoke) the function
greet();  // Output: Hello, World! 👋
greet();  // Output: Hello, World! 👋
// Call it as many times as you want!

Naming Rules

Rule Valid Invalid
Start with letter or underscore calculate_tax, _helper 123abc, -func
Letters, numbers, underscores only get_user_2 get-user, my func
Case-insensitive (but use consistent casing) myFunc() = MYFUNC()
Cannot redeclare an existing function Two function greet() blocks

✅ Naming Convention

PHP convention is snake_case for function names: get_user_by_id(), calculate_total(), send_email(). Use descriptive verb-noun names that say what the function does.

Functions Can Be Called Before Declaration

PHP reads the entire file before executing, so functions can be called above their declaration:

<?php
// This works! PHP loads all function declarations first.
say_hello();

function say_hello() {
    echo "Hi there!\n";
}
💡 Note: While this works, it's best practice to declare functions before using them for readability. Readers expect definitions before usage.

Parameters and Arguments

📖 Terminology

Parameter: The variable name in the function declaration (the placeholder).

Argument: The actual value passed when calling the function.

<?php
//            parameter ↓
function greet($name) {
    echo "Hello, $name! 👋\n";
}

//    argument ↓
greet("Alice");   // Hello, Alice! 👋
greet("Bob");     // Hello, Bob! 👋

Multiple Parameters

<?php
function calculate_rectangle_area($width, $height) {
    $area = $width * $height;
    echo "A {$width}×{$height} rectangle has an area of $area\n";
}

calculate_rectangle_area(5, 3);   // Area of 15
calculate_rectangle_area(10, 7);  // Area of 70

Default Parameter Values

Give parameters default values so they become optional:

<?php
function greet($name, $greeting = "Hello") {
    echo "$greeting, $name!\n";
}

greet("Alice");              // Hello, Alice!
greet("Bob", "Good morning"); // Good morning, Bob!
greet("Carol", "Hey");       // Hey, Carol!

⚠️ Default Parameters Must Come Last

Required parameters must come before optional ones:

// BAD — default before required
function bad_func($greeting = "Hello", $name) { ... }
// What does bad_func("Alice") mean? Is "Alice" the greeting or the name?

// GOOD — required first, then optional
function good_func($name, $greeting = "Hello") { ... }

Named Arguments (PHP 8)

PHP 8 lets you pass arguments by name, so order doesn't matter:

<?php
function create_user($name, $email, $role = "viewer", $active = true) {
    echo "Created $name ($email) as $role";
    echo $active ? " [active]\n" : " [inactive]\n";
}

// Positional arguments (traditional)
create_user("Alice", "alice@example.com", "admin", true);

// Named arguments — skip defaults, any order
create_user(
    email: "bob@example.com",
    name: "Bob",
    active: false
);
// Output: Created Bob (bob@example.com) as viewer [inactive]

// Mix positional and named (positional must come first)
create_user("Carol", "carol@example.com", role: "editor");

✅ When to Use Named Arguments

Named arguments shine when a function has many optional parameters and you only want to set one or two. They also make code more readable at the call site — you can see what each value means without checking the function definition.

Variadic Parameters (...)

Accept any number of arguments with the spread operator:

<?php
function sum(int ...$numbers): int {
    $total = 0;
    foreach ($numbers as $num) {
        $total += $num;
    }
    return $total;
}

echo sum(1, 2, 3);         // 6
echo sum(10, 20, 30, 40);  // 100
echo sum(5);                // 5

// You can also spread an array into arguments
$prices = [9.99, 24.50, 7.25];
echo sum(...$prices);       // Works! (if they were ints)

Pass by Reference (&)

By default, PHP passes arguments by value — the function gets a copy. To let a function modify the original variable, pass by reference:

<?php
// Pass by value (default) — original unchanged
function add_ten($number) {
    $number += 10;
}

$x = 5;
add_ten($x);
echo $x; // Still 5! The function modified a copy.

// Pass by reference — original IS modified
function add_ten_ref(&$number) {
    $number += 10;
}

$y = 5;
add_ten_ref($y);
echo $y; // 15! The function modified the original.
💡 Tip: Use pass-by-reference sparingly. Returning a value is almost always cleaner and less surprising. The main use case is when a function needs to modify multiple values (since you can only return one thing — unless you use an array).

Return Values

Most useful functions return a value rather than echoing directly. This lets the caller decide what to do with the result.

Basic Return

<?php
function add($a, $b) {
    return $a + $b;
}

$result = add(3, 4);
echo $result;        // 7
echo add(10, 20);    // 30

// Use the result in expressions
$total = add(5, 3) * 2;  // 16
if (add(2, 2) === 4) {
    echo "Math works!\n";
}

Return Ends the Function

return immediately exits the function — any code after it won't run:

<?php
function check_age($age) {
    if ($age < 0) {
        return "Invalid age";  // Function exits here
    }

    if ($age < 18) {
        return "Minor";        // Function exits here
    }

    return "Adult";            // Only reached if age >= 18
    echo "This never runs!";   // Dead code
}

Returning Multiple Values (via Array)

PHP functions can only return one value, but that value can be an array:

<?php
function get_min_max(array $numbers): array {
    return [
        'min' => min($numbers),
        'max' => max($numbers),
        'range' => max($numbers) - min($numbers),
    ];
}

$stats = get_min_max([4, 8, 1, 15, 3]);
echo "Min: {$stats['min']}\n";    // 1
echo "Max: {$stats['max']}\n";    // 15
echo "Range: {$stats['range']}\n"; // 14

// Or destructure directly:
['min' => $min, 'max' => $max] = get_min_max([4, 8, 1, 15, 3]);
echo "Min=$min, Max=$max"; // Min=1, Max=15

Functions That Return Nothing

If a function doesn't have a return statement (or uses return; with no value), it returns null:

<?php
function log_message($msg) {
    echo "[LOG] $msg\n";
    // No return statement — returns null
}

$result = log_message("Server started");
var_dump($result); // NULL

✅ echo vs. return — The Golden Rule

Functions should return data, not echo it. Let the caller decide how to use the result. This makes functions reusable — the same function can feed a web page, an API response, a log file, or a test.

// BAD — the function decides how to display
function get_greeting($name) {
    echo "<h1>Hello, $name!</h1>";
}

// GOOD — the function returns, caller decides
function get_greeting($name) {
    return "Hello, $name!";
}

// Caller can use it however they want:
echo "<h1>" . get_greeting("Alice") . "</h1>";
$json = json_encode(['greeting' => get_greeting("Alice")]);
error_log(get_greeting("Alice"));

Type Hints and Return Types

Type hints tell PHP (and other developers) exactly what types a function expects and returns. They catch bugs early and make code self-documenting.

Parameter Type Hints

<?php
function multiply(int $a, int $b): int {
    return $a * $b;
}

echo multiply(4, 5);    // 20
echo multiply(3, 7);    // 21
// multiply("hello", 5); // TypeError!

Available Type Hints

Type Description Example
int Integer function age(int $n)
float Floating point function price(float $p)
string String function greet(string $name)
bool Boolean function toggle(bool $on)
array Array function sum(array $nums)
callable Function/callback function apply(callable $fn)
?type Nullable (type or null) function find(?string $q)
void Returns nothing function log(): void
mixed Any type (PHP 8) function dump(mixed $v)
int|string Union type (PHP 8) function id(int|string $id)

Return Type Declarations

<?php
function divide(float $a, float $b): float {
    if ($b == 0) {
        throw new \InvalidArgumentException("Cannot divide by zero");
    }
    return $a / $b;
}

function is_adult(int $age): bool {
    return $age >= 18;
}

function get_names(): array {
    return ["Alice", "Bob", "Carol"];
}

// void — function doesn't return anything
function log_event(string $event): void {
    echo "[" . date("Y-m-d H:i:s") . "] $event\n";
    // No return statement (or just `return;`)
}

Nullable Types

<?php
// Parameter can be string OR null
function find_user(?string $email): ?array {
    if ($email === null) {
        return null;
    }

    // Simulate database lookup
    return ["name" => "Alice", "email" => $email];
}

$user = find_user("alice@example.com"); // Returns array
$user = find_user(null);                 // Returns null

Union Types (PHP 8)

<?php
// Accept either int or string
function format_id(int|string $id): string {
    return "ID-" . $id;
}

echo format_id(42);       // "ID-42"
echo format_id("abc");    // "ID-abc"

// Return int or false (common pattern)
function find_index(array $haystack, mixed $needle): int|false {
    $key = array_search($needle, $haystack);
    return $key !== false ? $key : false;
}

Strict Types

By default, PHP will try to coerce values (e.g., passing "5" to an int parameter works). To enforce strict type checking, add this to the top of your file:

<?php
declare(strict_types=1);

function add(int $a, int $b): int {
    return $a + $b;
}

echo add(3, 4);      // 7 ✅
echo add("3", "4");  // TypeError! ❌ Strings not accepted in strict mode

✅ Best Practice

Use declare(strict_types=1); in new projects. It catches type-related bugs at development time rather than letting them slip through to production. Combined with type hints, it makes PHP feel much more predictable.

Variable Scope

📖 Definition

Scope: The region of code where a variable is accessible. In PHP, functions create their own scope — variables inside a function are invisible outside, and vice versa.

Local Scope

Variables created inside a function are local — they exist only within that function:

<?php
function calculate() {
    $result = 42;  // Local variable
    echo "Inside: $result\n"; // 42
}

calculate();
echo "Outside: $result\n"; // ⚠️ Warning: Undefined variable

Functions Can't See Outside Variables

Unlike many other languages, PHP functions do not automatically have access to variables declared outside them:

<?php
$greeting = "Hello";

function say_hi() {
    echo $greeting; // ⚠️ Warning: Undefined variable!
    // PHP functions have their own scope
}

say_hi();
flowchart TD subgraph "Global Scope" A["$greeting = 'Hello'"] B["$count = 0"] end subgraph "Function Scope (say_hi)" C["Cannot see $greeting"] D["Has its own local variables"] end A -.->|"❌ Not visible"| C

The global Keyword (Use Sparingly!)

The global keyword lets a function access an outside variable — but it's generally considered bad practice:

<?php
$counter = 0;

function increment() {
    global $counter;  // Now we can access the outer $counter
    $counter++;
}

increment();
increment();
increment();
echo $counter; // 3

⚠️ Why global Is Bad Practice

Using global creates hidden dependencies — the function secretly depends on and modifies external state. This makes code harder to test, debug, and reuse. Instead, pass values as parameters and return results:

// BAD — hidden dependency on global
function calculate_tax() {
    global $price, $taxRate;
    return $price * $taxRate;
}

// GOOD — explicit inputs and outputs
function calculate_tax(float $price, float $taxRate): float {
    return $price * $taxRate;
}
$tax = calculate_tax(100.00, 0.0825);

Static Variables

A static variable retains its value between function calls but stays local to the function:

<?php
function get_next_id(): int {
    static $id = 0;  // Initialized once, persists across calls
    $id++;
    return $id;
}

echo get_next_id(); // 1
echo get_next_id(); // 2
echo get_next_id(); // 3
// $id is NOT accessible outside the function

Static variables are useful for counters, caches, and one-time initialization within a function.

Anonymous Functions (Closures)

An anonymous function is a function without a name. You can assign it to a variable, pass it as an argument, or return it from another function.

Basic Syntax

<?php
// Assign an anonymous function to a variable
$greet = function($name) {
    return "Hello, $name!";
};  // ← Note the semicolon! It's a statement.

echo $greet("Alice");  // Hello, Alice!
echo $greet("Bob");    // Hello, Bob!

Closures — Capturing Outside Variables

Anonymous functions can capture variables from the surrounding scope using use:

<?php
$taxRate = 0.0825;

$calculate_tax = function(float $price) use ($taxRate): float {
    return $price * $taxRate;
};

echo $calculate_tax(100);  // 8.25
echo $calculate_tax(250);  // 20.625

// The captured value is a COPY at the time of creation
$taxRate = 0.10;  // Changing the outer variable...
echo $calculate_tax(100);  // Still 8.25! (captured the old value)

// To capture by reference, use &
$counter = 0;
$increment = function() use (&$counter) {
    $counter++;
};

$increment();
$increment();
echo $counter; // 2 — the closure modified the outer variable

Functions as Arguments (Callbacks)

One of the most powerful uses of anonymous functions is passing them as arguments — these are called callbacks:

<?php
$numbers = [3, 1, 4, 1, 5, 9, 2, 6];

// Sort with a custom comparison
usort($numbers, function($a, $b) {
    return $b - $a; // Descending order
});
print_r($numbers); // [9, 6, 5, 4, 3, 2, 1, 1]

// Filter with a callback
$evens = array_filter($numbers, function($n) {
    return $n % 2 === 0;
});
print_r($evens); // [6, 4, 2]

// Transform with a callback
$doubled = array_map(function($n) {
    return $n * 2;
}, $numbers);
print_r($doubled); // [18, 12, 10, 8, 6, 4, 2, 2]

Writing Functions That Accept Callbacks

<?php
function apply_to_all(array $items, callable $transform): array {
    $result = [];
    foreach ($items as $key => $item) {
        $result[$key] = $transform($item);
    }
    return $result;
}

$names = ["alice", "bob", "carol"];

$uppercased = apply_to_all($names, function($name) {
    return strtoupper($name);
});
// ["ALICE", "BOB", "CAROL"]

$lengths = apply_to_all($names, function($name) {
    return strlen($name);
});
// [5, 3, 5]

// You can also pass built-in functions by name!
$capitalized = apply_to_all($names, 'ucfirst');
// ["Alice", "Bob", "Carol"]

Arrow Functions

PHP 7.4 introduced arrow functions — a shorter syntax for simple anonymous functions. They automatically capture variables from the outer scope (no use needed).

Syntax

fn($param) => expression

Comparison

<?php
// Anonymous function
$double = function($n) {
    return $n * 2;
};

// Arrow function — same thing, shorter
$double = fn($n) => $n * 2;

echo $double(5); // 10

Automatic Variable Capture

<?php
$taxRate = 0.0825;

// Anonymous function — needs "use"
$calc1 = function($price) use ($taxRate) {
    return $price * $taxRate;
};

// Arrow function — captures automatically
$calc2 = fn($price) => $price * $taxRate;

echo $calc1(100); // 8.25
echo $calc2(100); // 8.25

Arrow Functions with array_map, array_filter

Arrow functions really shine as concise callbacks:

<?php
$prices = [10.99, 24.50, 7.25, 18.00, 3.99];

// Filter prices over $10
$expensive = array_filter($prices, fn($p) => $p > 10);
// [10.99, 24.50, 18.00]

// Apply 10% discount
$discounted = array_map(fn($p) => $p * 0.90, $prices);
// [9.891, 22.05, 6.525, 16.2, 3.591]

// Sum of prices (using array_reduce)
$total = array_reduce($prices, fn($carry, $p) => $carry + $p, 0);
// 64.73

// Sort by price descending
$sorted = $prices;
usort($sorted, fn($a, $b) => $b <=> $a);
// [24.50, 18.00, 10.99, 7.25, 3.99]

Limitations of Arrow Functions

Feature Anonymous Function Arrow Function
Multiple statements ✅ Yes ❌ Single expression only
Outer variable capture Manual (use) Automatic (by value)
Modify outer variables ✅ With use (&$var) ❌ Always by value
Return keyword Required Implicit (expression is the return)

✅ When to Use Each

Arrow functions: Short, one-expression callbacks — filtering, mapping, sorting, simple transforms.

Anonymous functions: Multi-line logic, need to modify outer variables by reference, or complex processing.

Named functions: Reusable logic, anything complex enough to deserve a name, called from multiple places.

Hands-On Exercises

🏋️ Exercise 1: Temperature Converter

Objective: Write functions to convert between Fahrenheit, Celsius, and Kelvin.

Instructions:

  1. Create f_to_c($f) — Fahrenheit to Celsius: (F - 32) × 5/9
  2. Create c_to_f($c) — Celsius to Fahrenheit: C × 9/5 + 32
  3. Create c_to_k($c) — Celsius to Kelvin: C + 273.15
  4. Add type hints and return types
  5. Create a convert($temp, $from, $to) function that handles all conversions
💡 Hint

For the universal convert() function, first convert everything to Celsius as a common base, then convert from Celsius to the target unit. Use match for clean routing.

✅ Solution
<?php
function f_to_c(float $f): float {
    return ($f - 32) * 5 / 9;
}

function c_to_f(float $c): float {
    return $c * 9 / 5 + 32;
}

function c_to_k(float $c): float {
    return $c + 273.15;
}

function k_to_c(float $k): float {
    return $k - 273.15;
}

function convert(float $temp, string $from, string $to): float {
    // Normalize to Celsius first
    $celsius = match ($from) {
        'F' => f_to_c($temp),
        'C' => $temp,
        'K' => k_to_c($temp),
        default => throw new \InvalidArgumentException("Unknown unit: $from"),
    };

    // Convert from Celsius to target
    return match ($to) {
        'F' => c_to_f($celsius),
        'C' => $celsius,
        'K' => c_to_k($celsius),
        default => throw new \InvalidArgumentException("Unknown unit: $to"),
    };
}

// Test it
echo convert(212, 'F', 'C') . "°C\n";   // 100°C
echo convert(100, 'C', 'K') . " K\n";    // 373.15 K
echo convert(0, 'K', 'F') . "°F\n";      // -459.67°F

🏋️ Exercise 2: Array Utility Functions

Objective: Build a small library of reusable array functions.

Create these functions:

  1. array_average(array $nums): float — average of all values
  2. array_pluck(array $items, string $key): array — extract one field from an array of associative arrays
  3. array_group_by(array $items, string $key): array — group items by a field value
💡 Hint

For array_average, use array_sum() and count(). For array_pluck, loop and extract $item[$key]. For array_group_by, use the field value as the key for a new array.

✅ Solution
<?php
function array_average(array $nums): float {
    if (empty($nums)) {
        return 0.0;
    }
    return array_sum($nums) / count($nums);
}

function array_pluck(array $items, string $key): array {
    $result = [];
    foreach ($items as $item) {
        if (isset($item[$key])) {
            $result[] = $item[$key];
        }
    }
    return $result;
}

function array_group_by(array $items, string $key): array {
    $groups = [];
    foreach ($items as $item) {
        $groupKey = $item[$key] ?? 'unknown';
        $groups[$groupKey][] = $item;
    }
    return $groups;
}

// Test data
$employees = [
    ["name" => "Alice", "dept" => "Engineering", "salary" => 95000],
    ["name" => "Bob",   "dept" => "Marketing",   "salary" => 72000],
    ["name" => "Carol", "dept" => "Engineering", "salary" => 105000],
    ["name" => "Dave",  "dept" => "Marketing",   "salary" => 68000],
    ["name" => "Eve",   "dept" => "Engineering", "salary" => 88000],
];

// Get all names
$names = array_pluck($employees, "name");
print_r($names); // ["Alice", "Bob", "Carol", "Dave", "Eve"]

// Get all salaries and average
$salaries = array_pluck($employees, "salary");
echo "Average salary: $" . number_format(array_average($salaries), 2) . "\n";

// Group by department
$byDept = array_group_by($employees, "dept");
foreach ($byDept as $dept => $members) {
    $deptSalaries = array_pluck($members, "salary");
    $avg = number_format(array_average($deptSalaries), 2);
    echo "$dept: " . count($members) . " people, avg \$$avg\n";
}

🏋️ Exercise 3: Higher-Order Functions

Objective: Practice callbacks and arrow functions by creating a data pipeline.

Instructions:

  1. Create an array of product data (name, price, category, in_stock)
  2. Use array_filter with an arrow function to get only in-stock items
  3. Use array_filter again to get only items under $25
  4. Use array_map to apply a 15% discount
  5. Use usort to sort by price ascending
  6. Display the final list
✅ Solution
<?php
$products = [
    ["name" => "Widget",      "price" => 12.99, "category" => "Tools",    "in_stock" => true],
    ["name" => "Gadget",      "price" => 49.99, "category" => "Electronics","in_stock" => true],
    ["name" => "Doohickey",   "price" => 8.50,  "category" => "Tools",    "in_stock" => false],
    ["name" => "Thingamajig", "price" => 22.00, "category" => "Toys",     "in_stock" => true],
    ["name" => "Whatchamacallit","price" => 15.75,"category" => "Tools",   "in_stock" => true],
    ["name" => "Gizmo",       "price" => 31.00, "category" => "Electronics","in_stock" => true],
];

// Pipeline
$result = $products;

// Step 1: Only in-stock items
$result = array_filter($result, fn($p) => $p["in_stock"]);

// Step 2: Only items under $25
$result = array_filter($result, fn($p) => $p["price"] < 25);

// Step 3: Apply 15% discount
$result = array_map(fn($p) => [
    ...$p,
    "original_price" => $p["price"],
    "price" => round($p["price"] * 0.85, 2),
], $result);

// Step 4: Sort by price ascending
usort($result, fn($a, $b) => $a["price"] <=> $b["price"]);

// Display
echo "🏷️ On Sale (In Stock, Under \$25, 15% Off):\n\n";
foreach ($result as $product) {
    $orig = number_format($product["original_price"], 2);
    $sale = number_format($product["price"], 2);
    echo "  {$product['name']}: \$$orig → \$$sale\n";
}

🎯 Quick Quiz

Question 1: What does a function return if it has no return statement?

Question 2: What's the difference between a parameter and an argument?

Question 3: Why is global considered bad practice?

Question 4: What makes arrow functions different from anonymous functions?

Question 5: What does declare(strict_types=1); do?

Summary

🎉 Key Takeaways

  • Functions — Named, reusable blocks of code that accept input and return output
  • Parameters — Support defaults, named arguments (PHP 8), variadic (...), and pass-by-reference (&)
  • Return values — Always prefer return over echo; return arrays for multiple values
  • Type hints — Add int, string, array, ?type, int|string for safety; use strict_types for enforcement
  • Scope — Functions have their own scope; avoid global; use static for persistent local state
  • Anonymous functions — Assign to variables, pass as callbacks; capture outer variables with use
  • Arrow functions — Concise fn($x) => expr syntax; auto-capture; single expression only
  • Callbacks — Pass functions as arguments to array_map, array_filter, usort, and your own functions

📚 Additional Resources

🚀 What's Next?

With functions in your toolkit, you can now organize code beautifully. In Lesson 8: Arrays, you'll take a deep dive into PHP's most versatile data structure — indexed arrays, associative arrays, multidimensional arrays, destructuring, and the spread operator.

🎉 Congratulations!

You've completed Module 2: Core Language! You now have a solid command of PHP's fundamentals — variables, operators, control flow, loops, and functions. Time to level up with data structures!