PHP Zero to Advanced #4: Functions, Type Declarations, Arrow Functions & Scope
As your scripts grow, copying and pasting the same logic quickly becomes a maintenance nightmare. Functions let you name a piece of logic, give it inputs, and reuse it anywhere. Modern PHP functions can also declare the exact types they accept and return, which catches bugs early and makes your code self-documenting.
Defining and Calling Functions
A function is declared with the function keyword, a name, a parameter list, and a body. Use return to send a value back to the caller. A function without return returns null.
<?php
function greet($name)
{
return "Hello, $name!";
}
echo greet("Budi") . "\n";
echo greet("Siti") . "\n";
function printLine()
{
echo str_repeat("-", 20) . "\n"; // no return value
}
printLine();
Output:
Hello, Budi!
Hello, Siti!
--------------------
Function names are case-insensitive in PHP, but you should always call them exactly as declared. The common convention is camelCase.
Parameters: Defaults, Named Arguments, and Variadics
Parameters can have default values, which makes them optional. Since PHP 8 you can also pass named arguments, which lets you skip optional parameters and makes calls easier to read.
<?php
function formatPrice(float $amount, string $currency = "IDR", int $decimals = 0): string
{
return $currency . " " . number_format($amount, $decimals, ',', '.');
}
echo formatPrice(150000) . "\n";
echo formatPrice(19.99, "USD", 2) . "\n";
// Named arguments (PHP 8): skip optional parameters, any order
echo formatPrice(amount: 2500.5, decimals: 2) . "\n";
Output:
IDR 150.000
USD 19,99
IDR 2.500,50
A variadic parameter (...$name) collects any number of arguments into an array. The same ... operator also spreads an array into separate arguments.
<?php
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
echo sum(1, 2, 3) . "\n";
echo sum(...[10, 20, 30, 40]) . "\n"; // spread an array into arguments
echo sum() . "\n";
Output:
6
100
0
Type Declarations
Type declarations tell PHP (and your teammates) what a function expects and returns. By default, PHP tries to coerce values, for example turning the string "3" into the int 3. Adding declare(strict_types=1); as the very first statement in a file turns off this coercion for calls made from that file, so wrong types throw a TypeError.
<?php
declare(strict_types=1);
function multiply(int $a, int $b): int
{
return $a * $b;
}
echo multiply(3, 4) . "\n";
try {
echo multiply("3", 4);
} catch (TypeError $e) {
echo "TypeError: " . $e->getMessage() . "\n";
}
Output:
12
TypeError: multiply(): Argument #1 ($a) must be of type int, string given, called in /path/to/script.php on line 13
We recommend enabling strict types in every file you write. Here are the most useful type features:
| Syntax | Meaning |
|---|---|
?string | Nullable: a string or null |
int|string | Union type: either type is accepted |
void | The function returns nothing |
never | The function never returns (always throws or exits) |
mixed | Any type (use sparingly) |
callable, array, iterable | Special built-in types |
<?php
declare(strict_types=1);
function findUser(int $id): ?string // nullable: string or null
{
$users = [1 => "Budi", 2 => "Siti"];
return $users[$id] ?? null;
}
function toNumber(int|string $value): int|float // union types
{
return is_numeric($value) ? $value + 0 : 0;
}
function logMessage(string $message): void // returns nothing
{
echo "[LOG] $message\n";
}
var_dump(findUser(1), findUser(99));
var_dump(toNumber("4.5"), toNumber(10));
logMessage("Done");
Output:
string(4) "Budi"
NULL
float(4.5)
int(10)
[LOG] Done
Variable Scope
Functions in PHP have their own local scope. Unlike JavaScript, variables declared outside a function are not automatically visible inside it. Pass values in as parameters instead. A static variable keeps its value between calls.
<?php
$siteName = "My Blog";
function showTitle()
{
// $siteName is NOT visible here
return isset($siteName) ? $siteName : "(undefined)";
}
function showTitleWithParam(string $siteName): string
{
return $siteName; // pass data in explicitly
}
echo showTitle() . "\n";
echo showTitleWithParam($siteName) . "\n";
function counter(): int
{
static $count = 0; // keeps its value between calls
return ++$count;
}
counter();
counter();
echo "Called " . counter() . " times\n";
Output:
(undefined)
My Blog
Called 3 times
You may see the global keyword in old tutorials. Avoid it: hidden dependencies make code hard to test and reason about.
Passing by reference
Arguments are passed by value, so a function works on a copy. Prefix a parameter with & to modify the caller's variable directly. Use this sparingly; returning a new value is usually clearer.
<?php
function addItemCopy(array $cart, string $item): void
{
$cart[] = $item; // modifies a local copy
}
function addItemRef(array &$cart, string $item): void
{
$cart[] = $item; // modifies the original array
}
$cart = ["book"];
addItemCopy($cart, "pen");
echo count($cart) . "\n";
addItemRef($cart, "pen");
echo count($cart) . "\n";
Output:
1
2
Closures and Arrow Functions
An anonymous function (closure) has no name and can be stored in a variable or passed to another function. To use an outer variable, list it with use. Arrow functions (fn, PHP 7.4+) are a shorter syntax for single-expression closures and capture outer variables automatically, by value.
<?php
$taxRate = 0.11;
// Anonymous function (closure) capturing a variable with "use"
$addTax = function (float $price) use ($taxRate): float {
return $price * (1 + $taxRate);
};
echo $addTax(100000) . "\n";
// Arrow function: captures outer variables automatically
$discount = 0.2;
$applyDiscount = fn(float $price): float => $price * (1 - $discount);
echo $applyDiscount(50000) . "\n";
$prices = [10000, 25000, 40000];
$discounted = array_map(fn($p) => $p * (1 - $discount), $prices);
echo implode(", ", $discounted) . "\n";
Output:
111000
40000
8000, 20000, 32000
Arrow functions really shine with array functions like array_map and array_filter, which we will explore in Part 5.
Callables and recursion
Functions can accept other functions through the callable type. PHP 8.1 also added first-class callable syntax, strlen(...), to turn any function into a closure.
<?php
function applyTwice(callable $fn, int $value): int
{
return $fn($fn($value));
}
echo applyTwice(fn($x) => $x * 3, 2) . "\n";
$double = fn(int $x): int => $x * 2;
$strlen = strlen(...); // first-class callable syntax (PHP 8.1)
echo applyTwice($double, 5) . "\n";
echo $strlen("Bandung") . "\n";
Output:
18
20
7
A function can also call itself. This is called recursion, and it needs a base case to stop:
<?php
function factorial(int $n): int
{
return $n <= 1 ? 1 : $n * factorial($n - 1);
}
echo factorial(5) . "\n";
echo factorial(10) . "\n";
Output:
120
3628800
Tips and Common Mistakes
- Expecting outer variables inside a function. They are not in scope. Pass them as parameters or use
use/arrow functions for closures. - Placing
declare(strict_types=1)anywhere but the top. It must be the first statement in the file, otherwise PHP raises a fatal error. - Too many parameters. If a function needs more than about four arguments, consider named arguments, an options array, or a small class (Part 7).
- Doing too much in one function. A good function does one thing and its name says what that is:
calculateTotal(),isValidEmail(). - Forgetting the return type. Return types document intent and catch mistakes such as accidentally returning
null.
Exercise
Write two strictly typed functions: one that calculates Body Mass Index (weight in kg divided by height in meters squared), and one that returns a category for a BMI value. Call the first using named arguments.
<?php
declare(strict_types=1);
function bmi(float $weightKg, float $heightM): float
{
return round($weightKg / ($heightM ** 2), 1);
}
function bmiCategory(float $bmi): string
{
return match (true) {
$bmi < 18.5 => "Underweight",
$bmi < 25 => "Normal",
$bmi < 30 => "Overweight",
default => "Obese",
};
}
$value = bmi(weightKg: 70, heightM: 1.72);
echo "BMI: $value (" . bmiCategory($value) . ")\n";
Output:
BMI: 23.7 (Normal)
Bonus: add validation that throws an exception when the height is zero or negative. You will learn exceptions in depth in Part 8.
Summary
- Functions group reusable logic;
returnsends a value back. - Default values, named arguments, and variadics make parameters flexible.
- Use
declare(strict_types=1)plus parameter and return types for safer code. - Functions have their own scope; use parameters,
static, or references deliberately. - Closures capture variables with
use; arrow functionsfn() =>capture automatically.
In Part 5 we move into intermediate territory with arrays, PHP's most versatile data structure, and the powerful built-in array functions.
Related articles
PostgreSQL Zero to Advanced #1: Introduction & Setup
Start the series by understanding what a relational database is, installing PostgreSQL locally, and getting comfortable with psql commands like \l, \c and \dt.
Ruby on Rails Zero to Advanced #1: Ruby Basics
Start the Rails journey with the language underneath it: irb, variables, strings, puts vs p, and symbols explained with runnable examples.
Python Zero to Advanced #1: Introduction & Setup
Start your Python journey: what Python is, how to install it, how to use the REPL, print your first output, and run .py files from the terminal.