PHP Language Basics

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 example
<?php

$prefix = 'ORD';

A named function does not automatically inherit it:

PHP example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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:

PHP example
$normalisedPrefix = 'GLOBAL';

Inside the function:

  • create a local variable also named $normalisedPrefix;
  • assign it the uppercase version of the $prefix parameter;
  • 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 $normalisedPrefix and global $normalisedPrefix are separate variables.
Show solution

Solution

PHP example
<?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: Fix Hidden Global

Task

Refactor this function so it no longer uses hidden global state:

PHP example
<?php

$taxRate = 0.2;

function totalWithTax(int $subtotalCents): int
{
    global $taxRate;

    return $subtotalCents + (int) round($subtotalCents * $taxRate);
}

The refactored function should:

  • accept float $taxRate as its second parameter;
  • contain no global statement and no $GLOBALS access;
  • return the subtotal plus rounded tax.

Call it with (1000, 0.2) and (1000, 0.05). Print both results. The exact output should be:

1200
1050

Hints

  • The function signature should reveal every value that changes the result.
  • Pass each rate directly in its call.
  • The second call proves the calculation is not tied to one process-wide rate.
Show solution

Solution

PHP example
<?php

function totalWithTax(int $subtotalCents, float $taxRate): int
{
    $taxCents = (int) round($subtotalCents * $taxRate);

    return $subtotalCents + $taxCents;
}

echo totalWithTax(1000, 0.2), PHP_EOL;
echo totalWithTax(1000, 0.05), PHP_EOL;

// Prints:
// 1200
// 1050

Explanation

Both values that determine the answer now appear in the signature. A reader can understand totalWithTax(1000, 0.05) without searching for a global assignment elsewhere in the process.

$taxCents is local temporary state for one call. The second call creates a fresh local value using its own rate. No earlier call or unrelated global assignment can silently alter either result.

Task: Build Prefix Closure

Task

Create two closures that demonstrate when an outside value is captured.

Start with:

PHP example
<?php

$prefix = 'Order';

Then:

  1. assign a closure to $formatOrderId that accepts an integer ID, captures $prefix with use, and returns text such as Order #42;
  2. change $prefix to Invoice;
  3. assign the same closure shape to $formatInvoiceId, capturing the new value;
  4. call both closures with 42 and print their results.

The exact output should be:

Order #42
Invoice #42

Hints

  • Use function (int $id) use ($prefix): string for each closure.
  • Ordinary use ($prefix) captures the scalar value when the closure is created.
  • Reassigning $prefix does not rewrite the first closure's captured value.
Show solution

Solution

PHP example
<?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.