🧩 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 |
(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();
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:
- Create
f_to_c($f)— Fahrenheit to Celsius: (F - 32) × 5/9 - Create
c_to_f($c)— Celsius to Fahrenheit: C × 9/5 + 32 - Create
c_to_k($c)— Celsius to Kelvin: C + 273.15 - Add type hints and return types
- 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:
array_average(array $nums): float— average of all valuesarray_pluck(array $items, string $key): array— extract one field from an array of associative arraysarray_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:
- Create an array of product data (name, price, category, in_stock)
- Use
array_filterwith an arrow function to get only in-stock items - Use
array_filteragain to get only items under $25 - Use
array_mapto apply a 15% discount - Use
usortto sort by price ascending - 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
returnoverecho; return arrays for multiple values - Type hints — Add
int,string,array,?type,int|stringfor safety; usestrict_typesfor enforcement - Scope — Functions have their own scope; avoid
global; usestaticfor persistent local state - Anonymous functions — Assign to variables, pass as callbacks; capture outer variables with
use - Arrow functions — Concise
fn($x) => exprsyntax; auto-capture; single expression only - Callbacks — Pass functions as arguments to
array_map,array_filter,usort, and your own functions
📚 Additional Resources
- PHP Manual: Functions
- PHP Manual: Function Arguments
- PHP Manual: Anonymous Functions
- PHP Manual: Arrow Functions
- PHP Manual: Type Declarations
🚀 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!