Lesson 6 • Intermediate
Functions ⚡
By the end of this lesson you'll write reusable PHP functions with typed parameters and return types, default and named arguments, variadics, references, and arrow functions and closures — the building blocks of every program bigger than a script.
What You'll Learn in This Lesson
- Define and call functions, and return values with return
- Type parameters and return values: int, ?string, array, void, and union int|string
- Give parameters default values and call with PHP 8 named arguments
- Accept any number of arguments with a variadic ...$args
- Modify a caller's variable with pass-by-reference &$x
- Write arrow functions fn() => and closures with use, and handle scope with global
$variables and use loops. If $name or foreach is unfamiliar, do the Loops lesson first, then come back.php file.php. The Output panel under each example shows exactly what to expect.1️⃣ Defining, Calling & Returning
A function is a named block of code you can run on demand. You define it once with the function keyword, then call it by writing its name followed by (). A parameter is an input slot in the definition; an argument is the actual value you pass when calling. The keyword return hands a value back to whoever called the function — that's different from echo, which only prints. A return type like : string after the ) documents and enforces what comes back; : void means "returns nothing".
<?php
declare(strict_types=1); // turns type mismatches into errors — see Common Errors
// Define once, call as many times as you like.
// 'function' starts a definition; the name follows the same rules as a variable.
function sayHello(): void { // ': void' means "returns nothing"
echo "Hello, World!\n";
}
sayHello(); // call it — runs the body
sayHello(); // call again — that's the whole point of a function
// A PARAMETER is an input. ': string' after the ) is the RETURN TYPE.
function greet(string $name): string {
return "Hello, $name!"; // 'return' hands a value back to the caller
}
echo greet("Alice") . "\n"; // prints what greet() returned
echo greet("Bob") . "\n";
// A function that returns a number you can keep using.
function square(int $n): int {
return $n * $n;
}
$result = square(5); // store the returned value
echo "5 squared is $result\n";
?>Hello, World!
Hello, World!
Hello, Alice!
Hello, Bob!
5 squared is 25The declare(strict_types=1); line at the top makes PHP strict about types — pass a string where an int is expected and you get a clear error instead of a silent conversion. Put it at the very top of every file; it's a best-practice habit that catches bugs early.
2️⃣ Typed Parameters, Defaults & Named Arguments
Types in front of a parameter (string $name) say what kind of value is allowed. A default value ($role = "Student") makes a parameter optional — leave it out and the default is used. A nullable type ?string means "a string or null", and a union type int|string (PHP 8) means "either of these". With named arguments (PHP 8) you pass values by parameter name — introduce(role: "Mentor", name: "Carol") — so order stops mattering and your calls read like documentation.
<?php
declare(strict_types=1);
// DEFAULT VALUE: $role is "Student" unless the caller supplies one.
// Parameters with defaults must come AFTER ones without.
function introduce(string $name, string $role = "Student"): string {
return "$name is a $role";
}
echo introduce("Alice", "Developer") . "\n"; // both arguments given
echo introduce("Bob") . "\n"; // $role falls back to "Student"
// NULLABLE TYPE: '?string' means "a string OR null".
// UNION TYPE (PHP 8): 'int|string' means "an int OR a string".
function label(int|string $id, ?string $note = null): string {
$text = "ID: $id";
if ($note !== null) { // only add the note when one was passed
$text .= " ($note)";
}
return $text;
}
echo label(42) . "\n"; // note is null, so it's left off
echo label("A7", "priority") . "\n"; // id is a string here — both types allowed
// NAMED ARGUMENTS (PHP 8): name the parameter, so order no longer matters
// and you can skip optional ones in the middle.
echo introduce(role: "Mentor", name: "Carol") . "\n";
?>Alice is a Developer
Bob is a Student
ID: 42
ID: A7 (priority)
Carol is a Mentor3️⃣ Variadics & Pass-by-Reference
Sometimes you don't know how many arguments you'll get. A variadic parameter int ...$numbers collects every extra argument into one array, so sum(1, 2, 3) and sum(10, 20, 30, 40) both work. By default PHP passes a copy of each argument, so changing a parameter inside a function leaves the caller's variable untouched. Put an ampersand in front — &$total — to pass by reference instead, letting the function edit the original directly. Use references sparingly; usually returning a value is clearer.
<?php
declare(strict_types=1);
// VARIADIC parameter: '...$numbers' collects every extra argument into an array.
function sum(int ...$numbers): int {
return array_sum($numbers); // $numbers is a normal array here
}
echo "sum(1, 2, 3) = " . sum(1, 2, 3) . "\n";
echo "sum(10, 20, 30, 40) = " . sum(10, 20, 30, 40) . "\n";
echo "sum() = " . sum() . "\n"; // zero arguments is fine too
// PASS BY REFERENCE: '&$total' lets the function change the caller's variable.
// Without the &, PHP passes a COPY and the original is untouched.
function addTip(float &$total, float $tip): void {
$total += $tip; // this edits the caller's $bill directly
}
$bill = 40.0;
addTip($bill, 6.0); // no '=' needed — $bill is modified in place
echo "Bill after tip: $bill\n"; // 46
// Compare: pass by VALUE (no &) leaves the original alone.
function tryToReset(float $n): void {
$n = 0; // only the local copy changes
}
$score = 99.0;
tryToReset($score);
echo "Score is still: $score\n"; // still 99
?>sum(1, 2, 3) = 6
sum(10, 20, 30, 40) = 100
sum() = 0
Bill after tip: 46
Score is still: 994️⃣ Arrow Functions, Closures & Scope
PHP also lets you store a function in a variable — an anonymous function with no name. An arrow function fn($x) => $x * 2 (PHP 7.4+) is the short form: a single expression that automatically grabs any outer variables it mentions. A closure function ($x) use ($y) { ... } can hold a full multi-line body but must explicitly list outer variables in its use () clause. This matters because every function has its own scope: it can't see variables defined outside it unless you pass them in, capture them, or pull a global in with the global keyword.
<?php
declare(strict_types=1);
// ARROW FUNCTION (PHP 7.4+): 'fn(args) => expression'.
// It AUTOMATICALLY captures variables from the surrounding scope.
$factor = 3;
$scale = fn(int $x): int => $x * $factor; // $factor is captured automatically
echo "scale(5) = " . $scale(5) . "\n"; // 15
// Arrow functions shine as callbacks for array_map / array_filter.
$numbers = [1, 2, 3, 4, 5];
$squares = array_map(fn($n) => $n * $n, $numbers);
echo "squares: [" . implode(", ", $squares) . "]\n";
// CLOSURE (anonymous function): use 'function' and 'use' to capture variables.
// Unlike fn(), a closure does NOT capture automatically — you list what it needs.
$prefix = "Item";
$makeLabel = function (int $i) use ($prefix): string {
return "$prefix #$i"; // $prefix came from 'use ($prefix)'
};
echo $makeLabel(1) . "\n";
echo $makeLabel(2) . "\n";
// SCOPE & global: a function can't see outer variables unless told to.
$siteName = "LearnCodingFast";
function showSite(): void {
global $siteName; // pull the global variable into the function
echo "Welcome to $siteName\n";
}
showSite();
?>scale(5) = 15
squares: [1, 4, 9, 16, 25]
Item #1
Item #2
Welcome to LearnCodingFast5️⃣ Your Turn
Now you write the missing pieces. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
<?php
declare(strict_types=1);
// 🎯 YOUR TURN — fill in each blank marked ___ , then run it.
// 1) Add the RETURN TYPE so this function promises to return an int.
function area(int $w, int $h): ___ { // 👉 the type is int
return $w * $h;
}
// 2) Give $tax a DEFAULT VALUE of 0.2 so it can be left out.
function priceWithTax(float $price, float $tax = ___): float { // 👉 0.2
return $price * (1 + $tax);
}
echo "area(4, 5) = " . area(4, 5) . "\n";
echo "priceWithTax(100) = " . priceWithTax(100) . "\n"; // uses the default 0.2
// ✅ Expected output:
// area(4, 5) = 20
// priceWithTax(100) = 120
?>area(4, 5) = 20
priceWithTax(100) = 120___ with the type int and the second with the default 0.2, then run it.One more — this time finishing an arrow function and a closure that needs to capture a variable.
<?php
declare(strict_types=1);
// 🎯 YOUR TURN — finish the arrow function and the closure.
$numbers = [1, 2, 3, 4];
// 1) Write an ARROW FUNCTION that doubles its argument.
$double = ___ => $n * 2; // 👉 fn($n)
$doubled = array_map($double, $numbers);
echo "[" . implode(", ", $doubled) . "]\n";
// 2) This closure needs the $unit variable. Capture it with 'use'.
$unit = "kg";
$weigh = function (int $w) ___ : string { // 👉 use ($unit)
return "$w$unit";
};
echo $weigh(70) . "\n";
// ✅ Expected output:
// [2, 4, 6, 8]
// 70kg
?>[2, 4, 6, 8]
70kgfn($n) for the first blank and use ($unit) for the second so the closure can see $unit.Common Errors (and the fix)
- Your function returns
nullinstead of a value — you forgotreturn, or youecho'd the value inside the function instead of returning it. A function that never hits areturngives backnull. Add a return type like: intso PHP flags the mistake instead of failing silently. - "TypeError: ... must be of type int, string given" — with
declare(strict_types=1)at the top, passing the wrong type is an error, not a quiet conversion. Pass the right type, or change the parameter to a union likeint|stringif both are genuinely valid. - "Undefined variable" inside a function — functions have their own scope and can't see outer variables. Pass the value in as a parameter, or use
global $name;for a program-wide value. Variables defined outside simply don't exist inside unless you bring them in. - Your change inside a function "didn't stick" — PHP passes arguments by value (a copy) by default, so editing a parameter doesn't touch the caller's variable. If you really need to modify the original, declare the parameter by reference with
&$x— otherwise return the new value and assign it. - "Cannot use positional argument after named argument" — once you start naming arguments in a call, every following argument must also be named. Either name them all, or keep the unnamed ones first:
f("Carol", role: "Mentor").
Pro Tips
- 💡 Always add type hints and a return type. They catch bugs early and act as built-in documentation — the reader knows exactly what goes in and what comes out.
- 💡 Prefer parameters and return values over
globaland references. Functions that only touch their inputs and outputs are far easier to test and reason about. - 💡 Reach for
fn()for one-line callbacks inarray_map/array_filter, and a full closure withusewhen the body needs more than one expression.
📋 Quick Reference — PHP Functions
| Feature | Example | Since |
|---|---|---|
| Definition | function f($x) { ... } | PHP 4 |
| Typed param & return | function f(int $x): string | PHP 7.0 |
| Default value | function f($x = 10) | PHP 4 |
| Nullable type | ?string | PHP 7.1 |
| Variadic | function f(int ...$nums) | PHP 5.6 |
| By reference | function f(&$x) | PHP 4 |
| Arrow function | fn($x) => $x * 2 | PHP 7.4 |
| Closure | function ($x) use ($y) {} | PHP 5.3 |
| Union type | int|string | PHP 8.0 |
| Named arguments | f(name: "Carol") | PHP 8.0 |
Frequently Asked Questions
Q: What is the difference between an arrow function and a closure in PHP?
Both create anonymous (unnamed) functions, but they capture outer variables differently. An arrow function — fn($x) => $x * $factor — automatically captures variables from the surrounding scope, and its body must be a single expression. A closure — function ($x) use ($factor) { ... } — does not capture automatically: you list each variable you need in the use () clause, but in return it can hold a full multi-line body. Use an arrow function for short one-liners and a closure when you need more code or want to be explicit about what you capture.
Q: When should I pass a parameter by reference with &?
By default PHP passes a copy of each argument, so changing a parameter inside a function does not affect the caller's variable. Add an ampersand — function f(&$x) — when you genuinely want the function to modify the original variable in place, such as sorting an array or updating a running total. Most of the time you should instead return a new value and let the caller assign it; references make code harder to follow and should be used sparingly.
Q: Why does my function return null when I expected a value?
A PHP function returns null whenever it reaches the end without hitting a return statement, or hits a bare return; with no value. The two usual causes are forgetting return entirely (you echo'd the value inside the function instead of returning it) or a return that sits inside an if branch that did not run. Add a return type like : int — with declare(strict_types=1) PHP will then raise a TypeError if the function ever falls through to null, turning a silent bug into a clear error.
Q: What are named arguments and when are they useful?
Named arguments (PHP 8.0+) let you pass values by parameter name instead of position — introduce(name: 'Carol', role: 'Mentor'). They make calls self-documenting and let you skip optional parameters in the middle without passing every default before the one you care about. They are especially helpful for functions with several optional parameters, where a bare true, false, null at the call site tells the reader nothing.
Q: How do I use a variable from outside inside a function?
Functions have their own local scope, so they cannot see outer variables by default. The cleanest fix is to pass the value in as a parameter. If you truly need a program-wide value, declare global $name; inside the function to pull in the global variable, or use a static variable to remember a value between calls. Prefer parameters and return values — heavy use of global makes functions hard to test and reason about.
Mini-Challenge: Shopping-Cart Total
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This combines a variadic function, a return type, and a helper that formats the result.
<?php
declare(strict_types=1);
// 🎯 MINI-CHALLENGE: A shopping-cart total.
// No code is filled in — work from the steps below, then run it.
//
// 1. Write a VARIADIC function 'cartTotal(float ...$prices): float'
// that returns the sum of every price passed in (hint: array_sum).
// 2. Write a function 'format(float $amount): string' that returns the
// amount with a leading "$" and 2 decimals (hint: sprintf("$%.2f", $amount)).
// 3. Call cartTotal(9.99, 4.50, 2.00), pass the result to format(),
// and echo it.
//
// ✅ Expected output:
// $16.49
// your code here
?>cartTotal(...) and a format() helper, then echo the formatted total. Expected output: $16.49.🎉 Lesson Complete!
- ✅ Define a function with
function, call it with(), and hand back a value withreturn - ✅ Type parameters and returns —
int,?string,array,void, and unionint|string - ✅ Make parameters optional with defaults and call clearly with PHP 8 named arguments
- ✅ Accept any number of inputs with a variadic
...$args - ✅ Edit a caller's variable with pass-by-reference
&$x(and know when not to) - ✅ Store functions in variables with
fn() =>anduseclosures, and manage scope withglobal - ✅ Next lesson: Arrays & Collections — store and process lists of values, PHP's most-used data structure
Sign up for free to track which lessons you've completed and get learning reminders.