Scope, Globals, And Closures
PHP mainly distinguishes global scope from function scope. A variable assigned outside a function belongs to global scope. Parameters and variables assigned inside a named or anonymous function belong to that function call unless PHP is explicitly told otherwise.
Clear scope makes dependencies visible. A function is easier to understand when its inputs arrive through parameters and its result leaves through return, rather than when it silently reads or changes distant state.
Global And Local Scope Are Separate
This variable exists in global scope:
<?php
$prefix = 'ORD';
A named function does not automatically inherit it:
<?php
$prefix = 'ORD';
function formatOrderId(int $orderId): string
{
return "$prefix-$orderId";
}
formatOrderId(42);
Inside formatOrderId(), $prefix refers to a local variable that was never assigned, so PHP reports an undefined-variable warning. The earlier global assignment does not satisfy it.
Pass the dependency explicitly:
<?php
$prefix = 'ORD';
function formatOrderId(int $orderId, string $prefix): string
{
$normalisedPrefix = strtoupper($prefix);
return "$normalisedPrefix-$orderId";
}
echo formatOrderId(42, $prefix), PHP_EOL;
Output:
ORD-42
The function signature now reveals both required inputs. $normalisedPrefix is local temporary state. It is created for the current call and cannot collide with a variable of the same name outside the function.
Each Function Call Gets Local Variables
A local variable belongs to one function invocation:
<?php
function buildLabel(string $value): string
{
$label = "Value: $value";
return $label;
}
$first = buildLabel('A');
$second = buildLabel('B');
echo $first, PHP_EOL;
echo $second, PHP_EOL;
Each call creates its own $value parameter and $label local. The second call does not overwrite the string already returned by the first call.
Ordinary local variables do not preserve their values between calls. PHP creates them as execution enters the function and releases that local scope when execution leaves. Static local variables are a deliberate exception covered later in this lesson.
PHP Does Not Give if And Loops Their Own Scope
Some languages create a new variable scope for every brace-delimited block. PHP generally does not do that for if, foreach, while, or for blocks:
<?php
$isPaid = true;
if ($isPaid) {
$message = 'Ready to ship';
}
echo $message, PHP_EOL;
Because the branch runs, $message is available afterward in the same global or function scope.
This can still be unsafe. If $isPaid were false, the assignment would never happen and the later echo would read an undefined variable. Initialise a value before conditional assignment, or make every branch assign it:
<?php
$isPaid = false;
$message = 'Waiting for payment';
if ($isPaid) {
$message = 'Ready to ship';
}
echo $message, PHP_EOL;
The same issue applies to values assigned only inside loops: an empty iterable means the loop body never runs. Scope availability does not guarantee that an assignment happened.
Prefer Parameters To global
PHP's global keyword binds a local name to a variable in global scope:
<?php
$taxRate = 0.2;
function totalWithTax(int $subtotalCents): int
{
global $taxRate;
return $subtotalCents + (int) round($subtotalCents * $taxRate);
}
This works, but the signature hides an input. A reader cannot tell from totalWithTax(1000) that the result also depends on $taxRate. A test can change when unrelated code changes the global variable first.
Make the dependency part of the call:
<?php
function totalWithTax(int $subtotalCents, float $taxRate): int
{
return $subtotalCents + (int) round($subtotalCents * $taxRate);
}
$taxRate = 0.2;
echo totalWithTax(1000, $taxRate), PHP_EOL;
Output:
1200
Now different rates can be tested without mutating shared process state. The caller owns the configuration choice, and the function owns the calculation.
PHP also exposes global variables through the $GLOBALS superglobal array. Replacing global $taxRate with $GLOBALS['taxRate'] does not remove the hidden dependency; it only uses another access syntax. In ordinary application logic, explicit parameters are usually clearer than either form.
Treat Superglobals As Boundary Input
PHP provides superglobals such as $_GET, $_POST, $_SERVER, $_COOKIE, and $_ENV in every scope. They are available inside functions without a global declaration.
Availability does not make their values trustworthy or suitable as hidden dependencies. Request data may be absent, malformed, or controlled by a user. Read it near the entry point, validate it, then pass the intended value into application functions:
<?php
$nameInput = $_GET['name'] ?? '';
$name = is_string($nameInput) ? trim($nameInput) : '';
function greetingFor(string $name): string
{
if ($name === '') {
return 'Hello, guest';
}
return "Hello, $name";
}
echo greetingFor($name), PHP_EOL;
greetingFor() does not care whether the name came from a query string, command-line argument, test, or hard-coded example. That separation makes the rule reusable and keeps request handling at the boundary.
Static Local Variables Remember State
A local variable normally starts again on every call:
<?php
function ordinaryCounter(): int
{
$count = 0;
$count++;
return $count;
}
echo ordinaryCounter(), PHP_EOL;
echo ordinaryCounter(), PHP_EOL;
Both calls return 1.
Adding static changes the lifetime of that local variable:
<?php
function nextSequenceNumber(): int
{
static $number = 1000;
$number++;
return $number;
}
echo nextSequenceNumber(), PHP_EOL;
echo nextSequenceNumber(), PHP_EOL;
Output:
1001
1002
The name $number remains local to the function, but its value is initialised once and retained between calls during the process.
Static locals can be useful for tiny counters, memoised calculations, or implementation details. They also create hidden history: the result of the next call depends on earlier calls. That can surprise tests, workers, and long-running processes. Do not use a static local as general application storage or as a substitute for a database, cache, session, or explicit object state.
Closures Are Functions Stored As Values
An anonymous function has no declared function name. It can be assigned to a variable and called through that variable:
<?php
$formatOrderId = function (int $orderId): string {
return "Order #$orderId";
};
echo $formatOrderId(42), PHP_EOL;
$formatOrderId holds a callable value. The parentheses invoke that closure with 42 as its argument.
Closures are useful when behavior needs to be passed into another function, stored temporarily, or configured with a small amount of surrounding data. Later array lessons use them as callbacks for filtering and transforming values.
Capture Outside Values With use
Like a named function, a closure does not automatically see ordinary variables from its surrounding scope. List captured variables in a use clause:
<?php
$prefix = 'Order';
$formatOrderId = function (int $id) use ($prefix): string {
return "$prefix #$id";
};
echo $formatOrderId(42), PHP_EOL;
The parameter $id changes on each call. The captured $prefix configures the closure when it is created.
By default, use ($prefix) captures the current value:
<?php
$prefix = 'Order';
$formatOrderId = function (int $id) use ($prefix): string {
return "$prefix #$id";
};
$prefix = 'Invoice';
echo $formatOrderId(42), PHP_EOL;
The output is still Order #42 because the closure captured the earlier scalar value. This snapshot behavior is often easier to reason about than a closure whose result changes when outside state changes later.
PHP can capture by reference with use (&$prefix), allowing the closure to observe or modify the outer variable. That creates shared mutable state, so avoid it until the mutation is genuinely required and covered by focused tests.
Arrow Functions Capture Automatically
For a single expression, PHP supports arrow functions:
<?php
$prefix = 'SKU';
$formatSku = fn (string $code): string => "$prefix-$code";
echo $formatSku('A12'), PHP_EOL;
Arrow functions automatically capture used outer variables by value. They do not need use ($prefix), and their expression result is returned automatically.
Use an arrow function for a short expression. Use a normal closure when the body needs multiple statements or when an explicit use list makes dependencies easier to inspect.
Choose The Right State Boundary
Use these defaults:
- function parameters for values a caller must provide;
- local variables for temporary work inside one call;
- return values for sending results back;
- validated superglobal values only at request or process boundaries;
- static locals only when retained call history is intentional;
- closure captures for small configured callbacks;
- global state rarely, and only when the ownership and lifetime are explicit.
The goal is not to eliminate state. It is to make the owner, lifetime, and mutation of each value understandable.
Common Scope Mistakes
| Symptom | Likely cause |
|---|---|
| Undefined variable inside a function | Expected a global variable to be inherited automatically |
Value may be undefined after an if |
Assignment occurred only in a branch that might not run |
| Value may be undefined after a loop | Loop body did not run for an empty input |
| Function changes when unrelated code runs | Hidden global or $GLOBALS dependency |
| Test passes alone but fails after another test | Static or global state retained earlier history |
| Closure cannot see an outside variable | Missing use capture |
| Closure keeps an older scalar value | Capture by value happened before the outside reassignment |
| Closure unexpectedly changes outside state | Variable was captured by reference with & |
| Business function depends on request data | Superglobal was read deep inside application logic |
What You Should Be Able To Do
After this lesson, you should be able to distinguish global and function-local variables, explain why if and loops do not create separate PHP variable scopes, avoid conditionally undefined values, replace a hidden global with an explicit parameter, treat superglobals as boundary input, predict how a static local changes across calls, create and invoke a closure, capture a value with use, explain capture-by-value timing, and recognise a short arrow function.
Official references: PHP's manual pages for variable scope and anonymous functions.
Practice
Task: Prefix Formatter
Task
Write a function named formatTicketId() with this signature:
function formatTicketId(int $ticketId, string $prefix): string
Start with this global variable:
$normalisedPrefix = 'GLOBAL';
Inside the function:
- create a local variable also named
$normalisedPrefix; - assign it the uppercase version of the
$prefixparameter; - return the local prefix joined to the ticket ID with a hyphen.
Print formatTicketId(1042, 'support'), then print the global $normalisedPrefix. The exact output should be:
SUPPORT-1042
GLOBAL
Hints
- Pass the prefix into the function instead of trying to read a global prefix.
strtoupper()returns the uppercase string.- The local
$normalisedPrefixand global$normalisedPrefixare separate variables.
Show solution
Solution
<?php
$normalisedPrefix = 'GLOBAL';
function formatTicketId(int $ticketId, string $prefix): string
{
$normalisedPrefix = strtoupper($prefix);
return "$normalisedPrefix-$ticketId";
}
echo formatTicketId(1042, 'support'), PHP_EOL;
echo $normalisedPrefix, PHP_EOL;
// Prints:
// SUPPORT-1042
// GLOBAL
Explanation
The function receives its real dependency through $prefix. Its $normalisedPrefix temporary belongs to the function's local scope and exists only for that call.
The identically named global variable remains GLOBAL. Sharing a variable name does not connect two variables across PHP's global and function scopes. A global declaration or $GLOBALS access would create that connection, but neither is needed here.
Task: Build Prefix Closure
Task
Create two closures that demonstrate when an outside value is captured.
Start with:
<?php
$prefix = 'Order';
Then:
- assign a closure to
$formatOrderIdthat accepts an integer ID, captures$prefixwithuse, and returns text such asOrder #42; - change
$prefixtoInvoice; - assign the same closure shape to
$formatInvoiceId, capturing the new value; - call both closures with
42and print their results.
The exact output should be:
Order #42
Invoice #42
Hints
- Use
function (int $id) use ($prefix): stringfor each closure. - Ordinary
use ($prefix)captures the scalar value when the closure is created. - Reassigning
$prefixdoes not rewrite the first closure's captured value.
Show solution
Solution
<?php
$prefix = 'Order';
$formatOrderId = function (int $id) use ($prefix): string {
return "$prefix #$id";
};
$prefix = 'Invoice';
$formatInvoiceId = function (int $id) use ($prefix): string {
return "$prefix #$id";
};
echo $formatOrderId(42), PHP_EOL;
echo $formatInvoiceId(42), PHP_EOL;
// Prints:
// Order #42
// Invoice #42
Explanation
$formatOrderId captures the value Order when the first closure is created. Reassigning the outside $prefix variable afterward does not change that captured scalar value.
The second closure is created after the reassignment, so it captures Invoice. Both closures have an $id parameter supplied at call time, but each retains its own configured prefix.
Capturing with use (&$prefix) would share the outer variable by reference and produce different behavior. This exercise intentionally uses the more predictable default capture-by-value form.