PHP / Basic: values and request foundations / Lesson 4
Functions, scope, and closure capture
Write calculations with explicit parameters and results, then trace captured state. Distinguish ordinary functions, arrow functions, by-value captures, and deliberate references.
On this page
Lesson goals
- Define typed input and output contracts.
- Explain local scope and closure capture.
- Keep effects and error outcomes explicit.
A function expresses a small responsibility
A price calculation can accept integer cents and a valid count, then return integer cents without reading globals, parsing a request, or printing HTML. The caller owns those boundaries. Parameter and return declarations make the narrow contract visible, but they do not specify permitted ranges or rounding. Add domain validation where the function is a public boundary. Separate valid zero from an error outcome rather than returning zero for every failure. Clear functions are easier to reuse in CLI tests and later HTTP adapters without copying server-specific state into the calculation.
<?php
declare(strict_types=1);
function totalCents(int $price, int $count): int {
if ($price < 0 || $count < 0) { throw new InvalidArgumentException("negative input"); }
return $price * $count;
}
echo totalCents(125, 4), PHP_EOL;
try { totalCents(-1, 2); } catch (InvalidArgumentException $error) { echo $error->getMessage(), PHP_EOL; }
500
negative input
Integer declarations constrain types; the explicit branch rejects negative values. Real money models must also define overflow and rounding limits.
Local variables are not implicit global dependencies
Ordinary function scope does not automatically import a variable from the surrounding file. Pass required data as parameters rather than reaching for global. Constants, class members, and superglobals have their documented access rules, but making a calculation read a request superglobal still couples it to the host. Default arguments should express stable defaults, not conceal missing mandatory decisions. Named arguments can improve call readability, yet public parameter names become part of a caller-facing contract. Change them carefully when other code relies on named calls, and avoid mixing positional and named arguments without understanding ordering rules.
Capture a value or a reference deliberately
An anonymous function can import variables with use. A by-value capture observes the captured value as established when the closure was created; a by-reference capture can observe later changes to the referenced variable. Arrow functions capture used outer variables by value automatically, while object handles inside captured values can still reach mutable objects. Do not describe this as a universal deep snapshot. The example uses an integer so the difference is unambiguous. For delayed work, decide whether the submitted value or the newest value is intended, then make capture semantics match that requirement.
<?php
declare(strict_types=1);
$rate = 5;
$snapshot = fn(int $amount): int => $amount + $rate;
$live = function (int $amount) use (&$rate): int { return $amount + $rate; };
$rate = 10;
echo $snapshot(100), " ", $live(100), PHP_EOL;
105 110
The arrow retains five; the referenced variable later contains ten. Neither pattern should be chosen without defining the observation time.
A callable has timing and return obligations
A callback supplied to array_map executes as part of the current operation and must return the intended transformed value. A callback is not inherently asynchronous. Choose a named function for reused behavior and a small closure when surrounding context is meaningful. Avoid printing inside a transformation expected to return data, because output can escape into a response before headers are decided. By-reference parameters make mutation part of the API and should be documented, not introduced merely to avoid a return. Use a result object or array when multiple outputs have clear meanings.
Make ownership and failure paths reviewable
A useful flow is decode, validate, calculate, then render. Each function owns one interpretation boundary and communicates failure without inventing a successful-looking fallback. Keep external resources and current time as injected dependencies where they affect behavior so tests can be deterministic. A closure retaining a large response or resource can extend its lifetime, so avoid capturing more than needed and release owned callbacks when their owner is finished. Test the result and the rejected path, not merely that a function was called. Small signatures help only when their assumptions remain explicit.
Common errors and fixes
A calculation depends on a hidden global
- Why it happens
- Input ownership is implicit in surrounding state.
- How to fix it
- Pass the validated value as a parameter.
- Prevent it next time
- Test calls with independent explicit inputs.
A closure reads a surprising later value
- Why it happens
- Reference capture is used when a snapshot was intended.
- How to fix it
- Capture the required value or copy the required data deliberately.
- Prevent it next time
- Define whether observation occurs at creation or invocation.
A data helper sends output unexpectedly
- Why it happens
- Printing and returning are mixed in one contract.
- How to fix it
- Return data and render only at the response boundary.
- Prevent it next time
- Capture output in tests for reusable helpers.
Check your understanding
1. A closure must keep a submitted discount even if the form changes. How should it capture it?
Show worked solution
Validate the submitted scalar and capture it by value in the closure or pass it as an explicit argument. Do not capture a mutable request container by reference. If the payload contains objects, define the necessary copying policy because a by-value handle can still reach the same mutable object.
2. Why separate a total calculator from an HTML renderer?
Show worked solution
The calculation can be tested with exact cents without request or output side effects. The renderer owns context-specific escaping and presentation. This prevents a helper print from corrupting headers and allows the same domain rule to serve CLI checks or an API adapter.
Key takeaways
- Use explicit parameters and deliberate capture semantics.
- Returning data and emitting a response are different responsibilities.