Functions — Defining Reusable Logic
get_user_by_id, send_welcome_email, format_dose — and the high-level shape of your code starts to read like prose.use () to capture outer scope; arrow functions auto-capture. Keep each function focused on one job — if you have to scroll to read it, it's too long.The basic shape
function greet(string $name): string {
return "Hello, $name.";
}
echo greet('Eric'); // "Hello, Eric."
Reading the signature piece by piece: function greet defines a function called greet. (string $name) declares one parameter, expected to be a string. : string declares the function returns a string. Then the body builds a greeting and returns it.
Type declarations on parameters and return values are technically optional, but always write them. They serve two purposes — documentation (anyone reading knows what to pass and what to expect back) and enforcement (with declare(strict_types=1) on, PHP throws an error if someone passes the wrong type). Modern PHP with strict types is genuinely good; it catches a whole class of bugs at the boundary.
🐍 Python: Same syntax as Python type hints (def greet(name: str) -> str:). Difference: PHP enforces them at runtime when strict_types is on. Python type hints are advisory unless you run mypy. PHP is like having mypy permanently baked in.
Default arguments
You can give parameters default values, making them optional:
function greet(string $name, string $greeting = 'Hello'): string {
return "$greeting, $name.";
}
greet('Eric'); // "Hello, Eric."
greet('Eric', 'Hola'); // "Hola, Eric."
Important rule: defaults must come AFTER required arguments. You can't have a required parameter following an optional one — PHP wouldn't know how to match positional arguments to parameters. If you need to skip a default and pass a later one, use named arguments.
Named arguments (PHP 8+) — use these for clarity
When a function has multiple optional parameters, calling it with all positional arguments gets cryptic fast. Picture this call:
create_user('Eric', 'e@x.com', true, false, 'admin', null, 7);
What are all those values? You'd have to dig up the function definition every time to figure out which boolean is "admin" and which is "active." Named arguments fix this:
function create_user(
string $name,
string $email,
bool $admin = false,
bool $active = true,
): User {
/* ... */
}
create_user(name: 'Eric', email: 'e@x.com', admin: true);
Now the call site is self-documenting. admin: true is unambiguous. No more guessing. Use named args anytime you have multiple boolean parameters, or any function with more than three or four parameters — future readers (including you, three months from now) will be eternally grateful.
🐍 Python: Identical to Python keyword arguments: create_user(name='Eric', email='e@x.com', admin=True). Same idea, same syntax (just : in PHP vs = in Python). One of those PHP features that obviously came from looking over Python's shoulder.
Variadic arguments and nullable types
Two quick syntax features you'll see all over modern PHP. Variadic — accepts any number of args, collected into an array:
function sum(int ...$nums): int {
return array_sum($nums);
}
sum(1, 2, 3); // 6
sum(...[1, 2, 3]); // 6 — splat an array into args
Same operator (...) in both directions — collect into an array on the function side, unpack from an array on the call side.
Nullable types — a type that's either the declared type OR null:
function find_user(int $id): ?User {
// returns a User object, or null if not found
}
The ? before the type means "this type OR null." It forces the caller to handle the null case. Tiny piece of syntax, big safety win. Way better than the old PHP convention of returning false on failure (which leads to === false checks everywhere and accidental "0 was actually a valid result" bugs).
Scope — PHP isn't JavaScript
Important rule that surprises people coming from JavaScript: PHP functions have their own scope. Variables defined outside the function are NOT visible inside it.
$x = 10;
function bad() {
echo $x; // undefined — functions DON'T see outer scope
}
function good(int $x) {
echo $x; // pass it in
}
good($x);
This is the opposite of JavaScript, where inner functions can see outer variables freely. PHP doesn't work that way. The reason: PHP was designed to be safer-by-default — you can't accidentally clobber an outer variable from inside a function. Yes, global $x exists for accessing outer variables. No, don't use it. It makes code hard to test, hard to reason about, and impossible to refactor. Pass arguments. That's what they're for.
Closures and arrow functions
Sometimes you want a quick anonymous function — passing a short operation to array_map, defining a callback for usort. PHP has two flavors:
// Closure — explicit capture
$multiplier = 3;
$multiply = function(int $n) use ($multiplier) {
return $n * $multiplier;
};
// Arrow function — auto-captures, single expression only
$multiply = fn(int $n) => $n * $multiplier;
The use ($multiplier) clause on closures is mandatory if you want the closure to see an outer variable. PHP closures don't auto-capture — you have to explicitly list the variables you want available inside. This sounds like extra work, but it's actually a feature: closures stay self-contained and easy to reason about. You know exactly what they depend on.
Arrow functions, added in PHP 7.4, are a shortcut for single-expression closures. They DO auto-capture by value, so you don't need use. The trade-off: they're limited to one expression. For multi-line logic, go back to the longhand form. For one-liners (which most array_map callbacks are), arrow functions are cleaner.
🐍 Python: PHP arrow function fn($x) => $x * 2 ≈ Python lambda x: x * 2. Both single-expression only. The PHP quirk: regular closures need explicit use ($var) to capture outer scope, while Python lambdas auto-capture. PHP is more explicit, Python is more convenient.
Pass by value vs by reference
By default, when you pass a variable into a function, the function gets a copy. Mutating it inside doesn't affect the original:
function inc(int $x): int {
$x++;
return $x;
}
$n = 5;
inc($n); // returns 6
echo $n; // still 5
Add an & in the parameter list to pass by reference — now the function operates on the original variable:
function inc_ref(int &$x): void {
$x++;
}
$n = 5;
inc_ref($n);
echo $n; // 6 — mutated in place
Reference parameters are a power tool. Useful occasionally, but they make code harder to reason about because the function can change variables in the caller's scope. Default to "return the new value" rather than "mutate in place." Reach for references only when the API genuinely calls for it — the standard library's sort() is a good example, because it modifies the array in place.
One function, one job
Final rule, the most important one for code quality. Keep each function focused on a single job. The temptation in early PHP is to write 200-line functions that "do the page" — fetch data, validate, save, render, send emails, all in one giant blob. Resist that urge.
Each function should answer one clear question or perform one clear action. If you can't summarize what a function does in a single sentence, it's doing too much — split it. A useful heuristic that actually holds up: if you have to scroll to read the whole function, it's too long. A well-written function fits on your screen with room to breathe.
Mental model: think of functions like steps in a recipe. Each one is "saute the onions" or "preheat the oven." Not "make dinner." Small, focused, named clearly. Then "make dinner" becomes a short sequence calling those small functions.
Build: Extract Meds Helpers Into a Module
Time to refactor the meds page into something more professional. We'll pull all the formatting and logic out of meds.php and into a dedicated helpers file. Same behavior, way cleaner code, and you can reuse the helpers in any other page you build.
This is a great moment to practice the "extract method" refactor that you'll do constantly in real work. The goal is not to add new behavior — it's to make the existing behavior live in a cleaner shape. After this chapter, the meds page is mostly "fetch and render," and all the actual logic is in named, testable functions.
- Create
/home/erictey/server/lib/meds-helpers.php:<?php declare(strict_types=1); function status_color(string $status): string { return match ($status) { 'taken' => '#4eff7a', 'due' => '#5bf0ff', 'overdue' => '#ff4e6a', 'skipped' => '#ffa64e', default => '#b9adcf', }; } function status_label(string $status): string { return match ($status) { 'taken' => '✓ Taken', 'due' => '⏰ Due now', 'overdue' => '⚠ Overdue', 'skipped' => '— Skipped', default => ucfirst($status), }; } function format_dose(int $mg): string { if ($mg >= 1000) { return ($mg / 1000) . 'g'; } return "{$mg}mg"; } function sort_meds(array $meds, string $by = 'name'): array { usort($meds, fn($a, $b) => match($by) { 'dose' => $b['dose'] <=> $a['dose'], 'status' => $a['status'] <=> $b['status'], default => $a['name'] <=> $b['name'], }); return $meds; } function filter_meds(array $meds, string $status): array { if ($status === 'all') return $meds; return array_filter($meds, fn($m) => $m['status'] === $status); } function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } - At the top of
meds.php, require the helpers file:<?php declare(strict_types=1); require __DIR__ . '/lib/meds-helpers.php'; - Replace the inline sort and filter with calls to the helpers:
$meds = sort_meds($meds, $_GET['sort'] ?? 'name'); $meds = filter_meds($meds, $_GET['filter'] ?? 'all'); - In the template, replace direct calls with the helpers:
format_dose($med['dose'])andstatus_label($med['status'])instead of inline. - Refresh the page. It should work identically — but now the code is organized into a real library file.
Stretch goals:
- Add a
total_dose(array $meds): inthelper usingarray_reduce. - Add a
med_summary(array $meds): arraythat returns counts per status as an associative array. - Move
e()to its own file (lib/util.php) since every project needs it — not just meds.
What you flexed: Typed function signatures with parameter and return types, default arguments, match inside closures, array_filter with a captured query, require with __DIR__. You just made a reusable PHP module. The shape you've built here — "controller is small, logic lives in helpers" — is exactly how real PHP frameworks structure their code.