---
title: 'PHP Fundamentals'
source: 'https://academia.sh/en/courses/php-fundamentals'
language: en
updated: '2026-10-21T05:03:33+00:00'
license: 'CC BY-SA 4.0'
lessons: 14
outcomes:
    - "Explains the per-request execution model and measures that one request's state does not carry over to the next"
    - 'Distinguishes whether a problem is heard through the silent, return, warning or halting channel'
    - 'Writes programs with types, operators, control flow and functions'
    - 'Transforms data with array and string operations'
    - 'Receives form data, validates it and produces a response'
---

# PHP Fundamentals

In PHP a problem does not always report itself the same way: some operations quietly produce a value and carry on, some return false or null, some leave a warning and continue, some stop execution. The course measures every mechanism with a single question — through which channel is this problem heard? The execution model, syntax, types, operators, functions, arrays, strings and request data all pass through the same frame.

## Language and Execution

Every request running in a process built from scratch, code and text separated by tags, the conversion rules of variables and types, operators behaving according to type, forms of control flow, and what function signatures enforce.

### The PHP Execution Model

Source: https://academia.sh/en/courses/php-fundamentals/the-php-execution-model

Measuring which of the silent, return, warning and halts channels a problem is heard through in PHP; execution starting and ending per request, and an uncaught error cutting only that execution.

The C and C++ curriculum (M11) closed with one distinction: there, most problems were caught at compile time, the rest fell into undefined behavior, and the language said nothing. This course does not measure the compile stage in PHP. A file is read, interpreted and run as it executes, so almost every problem is heard inside execution. Even so, the same problem is not heard the same way everywhere.

This course asks a single question: **which channel is this problem heard through?** A missing key, a mismatched type or a file that cannot be found can each report itself in one of four different ways. This lesson first names those four ways and counts fourteen observations with a measurement core; then it measures where execution itself begins and ends, using the built-in server. The prerequisite Programming Fundamentals course (M01/K02) established the concepts of variable, type, loop and function; they are not explained again here.

#### Four Channels and the Measurement Core

The course uses four channels, and their names do not change from here on. **Silent**: a result is produced and there is no signal; the problem is visible only by looking at the result. **Return**: a function returns `false` or `null`, and hearing it is left to the caller. **Warning**: an error level (`E_WARNING`, `E_DEPRECATED`) is reported and execution continues. **Halts**: a `Throwable` is thrown or execution ends; if it is not caught, that execution is cut off right there.

This distinction does not overlap with the "who gives the guarantee" question measured in M08/K03: that course asked whose promise a behavior was, while here the measurement is of a signal, not a promise. It also does not overlap with the "where does the assurance end" question measured in M10/K05: that course looked for the limit of the compiler's checking, while here compilation is not measured and the course separates how problems appear during execution.

A rule decides the channel, not a label. The `channel.php` file below turns that rule into code: `listen()` first asks whether anything was thrown, then whether a warning was reported, then whether the result is `false` or `null`; if none of these apply, the problem is silent. Warnings are caught with `set_error_handler` and only the level name is kept, because the message text can change between versions and carries a real file path.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

In the two observations that run in a separate execution, the measure is the exit code: 255 shows that execution halted. The second file, `heard.php`, gathers the fourteen observations into one table. Each observation belongs to one of the later lessons; here only the channel they are heard through is examined.

```php
<?php
// heard.php — fourteen observations: which channel is this problem heard through
require 'channel.php';

function add_two(int $a, int $b): int
{
    return $a + $b;
}

$observations = [
    'halted execution'       => fn() => run_separately('echo "before\n"; throw new RuntimeException("stop");'),
    'syntax error'           => fn() => run_separately('echo "before\n"; $x = ;'),
    'integer overflow'       => fn() => listen(fn() => PHP_INT_MAX + 1),
    'leading-numeric string' => fn() => listen(fn() => '5 apples' + 1),
    'unmatched match'        => fn() => listen(fn() => match (3) { 1 => 'one', 2 => 'two' }),
    'extra argument'         => fn() => listen(fn() => add_two(1, 2, 3)),
    'key normalization'      => fn() => listen(fn() => ['1' => 'a', 1 => 'b', true => 'c']),
    'missing inner key'      => fn() => listen(fn() => [['name' => 'Ada']][0]['surname']),
    'element not found'      => fn() => listen(fn() => array_search('apple', ['pear', 'quince'])),
    'malformed json'         => fn() => listen(fn() => json_decode('{broken')),
    'missing query variable' => fn() => listen(fn() => $_GET['page']),
    'invalid form number'    => fn() => listen(fn() => filter_var('12a', FILTER_VALIDATE_INT)),
    'non-uploaded file'      => fn() => listen(fn() => move_uploaded_file('channel.php', 'target.php')),
    'missing include file'   => fn() => listen(fn() => include 'missing.php'),
];

$counts = ['silent' => 0, 'return' => 0, 'warning' => 0, 'halts' => 0];

printf("%-24s%-10s%s\n", 'observation', 'channel', 'evidence');
foreach ($observations as $name => $observe) {
    [$channel, $evidence] = $observe();
    $counts[$channel]++;
    printf("%-24s%-10s%s\n", $name, $channel, $evidence);
}

echo "\n";
foreach ($counts as $channel => $n) {
    printf("%-10s%d\n", $channel, $n);
}
echo 'total observations: ', array_sum($counts), "\n";
```

```
observation             channel   evidence
halted execution        halts     lines out 1, exit 255
syntax error            halts     lines out 0, exit 255
integer overflow        silent    result float 9.2233720368548E+18
leading-numeric string  warning   E_WARNING, result int 6
unmatched match         halts     UnhandledMatchError
extra argument          silent    result int 3
key normalization       silent    result array(1)
missing inner key       warning   E_WARNING, result null
element not found       return    result false
malformed json          return    result null
missing query variable  warning   E_WARNING, result null
invalid form number     return    result false
non-uploaded file       return    result false
missing include file    warning   E_WARNING, result false

silent    3
return    4
warning   4
halts     3
total observations: 14
```

Of the fourteen observations, 3 were heard through the silent channel, 4 through return, 4 through warning, and 3 halted execution. **LE1.** This table is the result of an execution run in the sandbox without a loaded `php.ini`; the measurement depends on no ini value, and when a setting is needed it is passed with the `-d` flag. The rows of the same table stay the same on another installation, because what is printed is not the message text but the name of the level constant, the class name of the exception and the exit code.

Three things stand out in the table. In the silent channel the result is not wrong, only different: when the integer limit was exceeded, the type became `float` without a signal. The return channel leaves the signal to the caller: `array_search` returned `false` when it found nothing, and if no one asks, no one hears it. The warning channel announces but does not halt: the missing include file returned `false` and execution continued. Order matters too: a missing file pulled in with `require` first reports a warning and then throws `Error`; because the channel that cuts execution is the strongest channel, this mechanism falls in the halts channel.

#### Execution Starting and Ending per Request

The channel this lesson measures is halts, and it comes down to one question: where does execution end? The built-in server (`php -S`) runs the file from the beginning for every request. Nothing is carried over from the previous request's variables, from `static` values, or from unfinished work of an earlier request. Where the lifetime of values ends was measured as a separate axis in M10/K02; that question is not asked here, and only the boundaries of one execution are drawn.

The first file increments a global variable and a `static` variable inside a function. The second divides by an integer and throws an uncaught error if the divisor is zero. Both files are given as files only, without output blocks; the script that follows measures them on the server.

```php
<?php
// counter.php — a global variable and a static restart on every request
$visit = 0;

function next_count(): int
{
    static $counter = 0;
    return ++$counter;
}

$visit++;
echo 'visit=', $visit, ' c=', next_count(), "\n";
```

```php
<?php
// division.php — if the divisor is zero, an uncaught error; the request ends with 500
$n = (int) ($_GET['n'] ?? 0);
$remainder = 100 % $n;
echo 'remainder=', $remainder, "\n";
```

The server starts with `-d display_errors=0`, so the error text does not mix into the response body and only the status code is measured. The script requests the counter file three times, and the division file first with `n=0`, then `n=2`, then `n=0` again. The last line runs the same division file on the command line and prints the exit code.

```bash
# request.sh — execution per request: counter, division and the same file on the command line
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/counter.php && break; sleep 0.1; done
for i in 1 2 3; do curl -s http://127.0.0.1:8081/counter.php; done
curl -s -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/division.php?n=0'
curl -s -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/division.php?n=2'
curl -s -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/division.php?n=0'
kill $pid
php -d display_errors=0 division.php; echo "exit=$?"
```

```
visit=1 c=1
visit=1 c=1
visit=1 c=1
status=500
remainder=0
status=200
status=500
exit=255
```

**LE2.** In three separate requests the global variable and the `static` variable both gave `1` every time: even if the server keeps its own process open, every request runs the file from the beginning, and the state of the previous request does not pass to the next. The server's process model does not determine this result; what was measured is the behavior of the built-in server. How another installation achieves per-request execution is the subject of M16/K01.

The first `n=0` request gave `500`, because `100 % 0` throws a `DivisionByZeroError` and nothing catches it; since it was thrown before any output was printed, the server sets the status code to `500`. The `n=2` request right after it gave `remainder=0` and `200`. The error was inside a separate execution and left no trace for the next one.

#### Silencing Does Not Change the Channel

Code that does not want to hear a warning knows two ways: the `@` operator and `error_reporting(0)`. Neither removes the problem; they only affect where the report goes. If an error handler is registered, the warning still reaches it. The file below puts three reads side by side: unsilenced, with `@`, and after `error_reporting(0)`. Each read reads an undefined array key.

```php
<?php
// silence.php — @ and error_reporting(0) do not remove the warning, nor change whether it reaches the handler
$reports = 0;
set_error_handler(function (int $level) use (&$reports): bool {
    $reports++;
    return true;
});

$items = [];

$a = $items['missing'];
echo 'unsilenced:         reports: ', $reports, ', value: ', var_export($a, true), "\n";

$b = @$items['missing'];
echo 'with @:             reports: ', $reports, ', value: ', var_export($b, true), "\n";

error_reporting(0);
$c = $items['missing'];
echo 'error_reporting(0): reports: ', $reports, ', value: ', var_export($c, true), "\n";
```

```
unsilenced:         reports: 1, value: NULL
with @:             reports: 2, value: NULL
error_reporting(0): reports: 3, value: NULL
```

**LE3.** All three reads gave the same value, `NULL`: the problem was the same in all three cases, and only the visibility of the report changed. The counter is the number of reports that reached the error handler. Silencing does not move a problem in the warning channel to the silent channel; it only prevents it from being written to the screen or the log. Code that does not hear the problem still has `NULL` in hand, and the code cannot know whether this value means false.

#### Exit and the Shutdown Function

`exit` ends execution on that line and the following lines do not run; this is in the halts channel. But execution does not go completely quiet: a function registered with `register_shutdown_function()` runs even after `exit` is called. The file below measures the order.

```php
<?php
// exit.php — exit cuts execution, the shutdown function still runs
register_shutdown_function(function () {
    echo "shutdown\n";
});

echo "a\n";
exit(3);
echo "b\n";
```

```
a
shutdown
```

**LE4.** The output was `a` and `shutdown`; `b` was never printed. Execution was cut, but the registered function ran before the request closed. This shows that even when an uncaught error or `exit` cuts a request, a window is left in which cleanup code can run. The same window also opens for code that cannot see why execution was cut: the shutdown function runs without knowing the cause of the problem.

#### The Bounding Measurement

The scope of the halts channel is one execution, not the whole program. **LE5.** On the built-in server a request is one execution: an uncaught error cuts only that request and the server stays up, and the next request starts clean. On the command line, execution is the whole program: the same `division.php` file gave `exit=255` there and nothing was left to run afterward. The channel is the same; the scope differs.

So the word "halts" must be read with two qualifications. The first is that halts holds inside the boundary of an execution: how the program is run determines where that boundary lies. The second is that the status code depends on where the error is thrown relative to the output; in this lesson the error was thrown before anything was printed and `500` came back. The Syntax and Tags lesson measures what this order changes.

A boundary can also be drawn inside an execution: a `Throwable` caught with `try` and `catch` stops being halts and passes into the control of whoever caught it. `listen()` in the core does this; even so, the evidence for two of the three `halts` rows in the table is still the exit code, because those two observations run in a separate execution.

#### Summary

- In PHP a problem is heard through one of the silent, return, warning or halts channels; the channel is decided not by a label but by the order in which `listen()` asks.
- Of the fourteen observations, 3 were heard through the silent channel, 4 through return, 4 through warning, and 3 halted execution.
- This lesson's channel is halts: an uncaught error cuts execution right there.
- On the built-in server every request runs the file from the beginning; the global variable and `static` start over on every request.
- Halts cuts only that execution: on the server the next request starts clean, while on the command line execution is the whole program.

#### Next Step

It was measured that an execution can be cut by an error, but two separate forms of cutting sit side by side in the table: `halted execution` printed one line and exited with 255, while `syntax error` printed no line and exited with 255. The Syntax and Tags lesson measures this difference: when does a file never start running at all?

### Syntax and Tags

Source: https://academia.sh/en/courses/php-fundamentals/syntax-and-tags

A file with a syntax error never starts running, how that differs from an error thrown during execution, and how code and template mix through tags in a PHP file.

The Execution Model lesson left two rows side by side in the core table: `halted execution` printed one line and exited with 255, while `syntax error` printed no line and exited with 255. Both belong to the halts channel, but they say two different things at once: one is the cutting of an execution that is already running, the other is an execution that could never start. This lesson measures the difference.

The difference comes from how PHP handles a file. Before the file is run, it is read in full and parsed according to the rules of the language. If parsing fails, not a single line of the file runs. Tags decide where a file begins and ends: PHP counts only the part between the opening and closing tags as code, and writes everything outside them to the output as it is. This lesson measures the two subjects together, because both concern when a file starts running.

#### A File That Never Starts Running

Two small files carry the same two steps: first print a line, then run into a problem. In the first, the problem appears during execution: a `RuntimeException` is thrown and not caught. In the second, the problem is against the language itself: the statement `$x = ;` expects a value and meets a semicolon. The third file is sound and serves as the comparison. All three are given as files only, without output blocks.

```php
<?php
// fatal.php — prints a line first, then throws an uncaught exception
echo "before\n";
throw new RuntimeException('stop');
```

```php
<?php
// broken.php — syntax error: the statement $x = ; has no value
echo "before\n";
$x = ;
```

```php
<?php
// sound.php — valid syntax, prints only one line
echo "before\n";
```

The script below runs each file in a separate execution. `display_errors=0` keeps the error text out of the output; only the lines the file printed and the exit code remain. `php -l` only parses the file without running it and reports the exit code.

```bash
# separate.sh — the same two steps, two different stops: thrown during execution and failed in parsing
php -d display_errors=0 fatal.php; echo "fatal exit=$?"
php -d display_errors=0 broken.php; echo "broken exit=$?"
php -l sound.php >/dev/null 2>&1; echo "sound -l=$?"
php -l broken.php >/dev/null 2>&1; echo "broken -l=$?"
```

```
before
fatal exit=255
broken exit=255
sound -l=0
broken -l=255
```

**LE6.** Both files exited with 255, but `fatal.php` managed to print its first line and `broken.php` did not. What is measured is the presence of output, not the text of the error message: the message changes between versions and carries a file path. `php -l` gave 0 for the sound file and 255 for the broken one; that is, the parsing result can be learned without running the file at all.

The same difference turns into a status code on the built-in server. When `broken.php` is requested, the file cannot start running, so the server has no response body and the status code is `500`. `fatal.php` first prints the `before` line; the body has begun, and the error that comes afterward can no longer change the status code. The server starts with `-d display_errors=0`, so no error text appears in the body.

```bash
# server.sh — a file with a syntax error gives 500, a file that throws during execution gives 200 with a partial body
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/sound.php && break; sleep 0.1; done
curl -s -w 'status=%{http_code}\n' http://127.0.0.1:8081/sound.php
curl -s -w 'status=%{http_code}\n' http://127.0.0.1:8081/fatal.php
curl -s -w 'status=%{http_code}\n' http://127.0.0.1:8081/broken.php
kill $pid
```

```
before
status=200
before
status=200
status=500
```

**LE7.** Three requests gave three different results: the sound file gave `before` and `200`, the file that failed during execution gave the same `before` line and still `200`, and the file with the syntax error gave `500` with no body. The `200` on the second line does not mean the error was lost; because the error came after the body had begun, it did not reach the status code. For this reason the status code is not a reliable measure of whether a problem was heard; it reflects only whether the error came before or after the output.

#### Mixing Code and Template

A PHP file can carry code and plain text interleaved. The opening tag `<?php` starts a code section and the closing tag `?>` ends it; everything between the two is parsed, and everything outside goes to the output unchanged. `<?=` is the short form used to print a value: `<?= $product ?>` does the same job as `<?php echo $product; ?>`. In this way a file can be written as both a script and a template. The example below produces an HTML list with a loop over an array. The `foreach (...):` and `endforeach;` form is the notation meant for blocks that spread across tags instead of curly braces.

```php
<?php
// template.php — mixing code and template, with the short echo tag
$products = ['pencil', 'notebook', 'eraser'];
?>
<ul>
<?php foreach ($products as $product): ?>
  <li><?= $product ?></li>
<?php endforeach; ?>
</ul>
```

```
<ul>
  <li>pencil</li>
  <li>notebook</li>
  <li>eraser</li>
</ul>
```

**LE8.** A single newline directly after each `?>` tag is not written to the output; PHP counts it as part of the closing tag. For this reason the output has no blank lines left over from the code lines, and the three `<li>` lines stack one under another with no gap. This behavior holds only for a single newline; if two newlines follow the tag, the second one enters the output.

This result matters at the very end of a file. In a file that carries only PHP code, no closing tag is written; the file is valid without a closing tag for the opening tag. The reason is measurable: a newline or space that slips in after the closing tag enters the output. The script below writes the same one-line program in three forms and measures how many bytes each produces.

```bash
# closing.sh — a newline left after the closing tag at the end of a file adds bytes
printf '<?php\necho "a";\n' > open.php
printf '<?php\necho "a";\n?>\n' > closed1.php
printf '<?php\necho "a";\n?>\n\n' > closed2.php
for d in open closed1 closed2; do
  printf '%s %s\n' "$d" "$(php $d.php | wc -c | tr -d ' ')"
done
```

```
open 1
closed1 1
closed2 2
```

**LE9.** The file without a tag and the file with a closing tag followed by one newline produced one byte, the `echo "a"` itself; the file with a closing tag followed by two newlines produced two bytes. The harm of the extra byte is that it is invisible: every byte that enters the output before the response headers are sent makes setting headers too late. For this reason, if the file carries only code, the final tag is not written.

#### A Line Comment Also Ends at the Closing Tag

Tags have one more consequence that they process before parsing: a closing tag ends the code section even inside a line comment. Everything after the tag, including the rest of the comment, is plain text; the following lines stop being code. No syntax error appears in this case, because there is no code left to parse, and the file runs without trouble.

```php
<?php
// comment.php — a closing tag inside a line comment ends the code section
echo "a\n"; // comment ?> this part is text
echo "b\n";
```

```
a
 this part is text
echo "b\n";
```

**LE10.** The output holds the `a` line, then the part of the comment after the tag, and finally the line `echo "b\n";` itself, that is, its source text, as plain text; `b` was never printed. There was no signal at all: no warning, no status code. This observation falls in the silent channel of the table and does not change this lesson's channel; it only shows that tags are processed before parsing, independently of the meaning of the comment.

#### Case Sensitivity

In PHP, whether a name is sensitive to upper and lower case depends on the kind of name. Keywords and function names are insensitive; variable names and constant names defined with `define()` are sensitive. A typo is therefore not noticed in one place and noticed in another. The file below tries four names with upper and lower case differences; the error handler keeps a counter.

```php
<?php
// case.php — function names are case-insensitive, variable and constant names are sensitive
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$name = 'Ada';
define('LIMIT', 5);

ECHO 'keyword: worked in uppercase too', "\n";
echo 'STRLEN:     ', STRLEN('abc'), "\n";
echo 'notices:    ', $notices, "\n";

$seen = $Name;
echo 'notices:    ', $notices, ' (after reading $Name)', "\n";
echo 'defined(limit): ', var_export(defined('limit'), true), "\n";
echo 'defined(LIMIT): ', var_export(defined('LIMIT'), true), "\n";
```

```
keyword: worked in uppercase too
STRLEN:     3
notices:    0
notices:    1 (after reading $Name)
defined(limit): false
defined(LIMIT): true
```

**LE11.** `ECHO` and `STRLEN` worked even though they were written in uppercase, and produced no notice. `$Name` is a variable separate from `$name`; when it was read, a warning was heard and the counter rose to one. In constant names too, `limit` and `LIMIT` are different: only the exact spelling was found. So the same typo is silent in a function name and a warning in a variable name; the kind of name that was misspelled decides which channel the error is heard through.

#### Single and Double Quotes

A string is written in two forms in PHP, and the two are not the same. In a single-quoted string, variables and escape sequences are not interpreted; only `\'` and `\\` carry a special meaning. In a double-quoted string a variable is substituted and escape sequences such as `\n` are resolved. The file below writes the same text in both forms and counts their lengths.

```php
<?php
// strings.php — single quotes resolve neither variables nor escapes, double quotes do
$to = 'Ada';
$single = 'welcome $to\n';
$double = "welcome $to\n";
$braced = "welcome {$to}x";

echo 'single:  ', $single, "|\n";
echo 'double:  ', $double, "|\n";
echo 'braced:  ', $braced, "|\n";
echo 'single length: ', strlen($single), "\n";
echo 'double length: ', strlen($double), "\n";
echo 'backslash: ', strlen('a\\b'), ' ', strlen('a\nb'), ' ', strlen("a\nb"), "\n";
```

```
single:  welcome $to\n|
double:  welcome Ada
|
braced:  welcome Adax|
single length: 13
double length: 12
backslash: 3 4 3
```

**LE12.** The single-quoted string came out with `$to` and `\n` as they are, so its length was thirteen. In the double-quoted string `Ada` took the place of `$to` and `\n` became a single newline character. The curly braces decide where the variable name ends. The last line shows the difference at the character level: in single quotes `\n` is two characters, in double quotes one. Neither produces a notice; a `\n` written in single quotes silently fails to give the expected newline.

#### Comment Forms

PHP recognizes three comment forms: `#` and `//` run to the end of the line, and `/* */` runs until its closing. Multi-line comments do not nest: the first `*/` ends the comment. The file below surrounds five `echo` statements with different comments.

```php
<?php
// comment_kinds.php — measures where each comment form ends
echo "1\n"; # line-end comment
echo "2\n"; // line-end comment
echo "3\n"; /* inline comment */ echo "4\n";
/* outer comment /* inner comment */ echo "5\n";
```

```
1
2
3
4
5
```

**LE13.** All five lines were printed. When `/*` opened a second time on the fourth line, the comment did not deepen; the first `*/` closed the comment and `echo "5\n"` ran as code. Code that relies on closing a block nested inside another runs whatever comes after the closing. This too is in the silent channel: there is no error or warning, only the amount of running code has grown.

#### echo and print

PHP has two language constructs that write to the screen, and they are not the same. `echo` takes several values separated by commas and returns no value. `print` takes a single value and always returns `1`; for this reason it can be used inside an expression. The file below measures the two behaviors and the way an array element is written inside double quotes.

```php
<?php
// output.php — print returns a value, echo takes many arguments, an array element resolves in double quotes
$result = print "a\n";
echo 'print returned: ', $result, "\n";
echo 'one', 'two', "\n";

$items = [10, 20];
echo "array: $items[0]-$items[1]\n";
echo 'in single quotes: $items[0]', "\n";
```

```
a
print returned: 1
onetwo
array: 10-20
in single quotes: $items[0]
```

**LE14.** `print` wrote the line and returned `1`; `echo` wrote the two strings one after another with nothing between them. In the double-quoted string `$items[0]` was replaced by the value of the element, and in the single-quoted one it stayed as it was. None of these produces a notice; the difference can be seen only by looking at the output.

#### The Bounding Measurement

The rule that a file with a syntax error never runs applies to each file separately. **LE15.** If a file pulls in another file with `include`, the faulty one is the pulled-in file and the main file has already started running. Because the pulled-in file is parsed during execution, the error is thrown at that point as a `ParseError`, and `try` with `catch` can catch it. The file below pulls in the `broken.php` written earlier.

```php
<?php
// main.php — a file with a syntax error is included; the main file has started running
echo "main started\n";
try {
    include 'broken.php';
} catch (ParseError $error) {
    echo 'caught ', get_class($error), "\n";
}
echo "main finished\n";
```

```
main started
caught ParseError
main finished
```

The main file printed all three of its lines, while the `before` line of `broken.php` did not appear: the pulled-in file again ran none of its own lines. The rule stayed the same and its scope changed: parsing is done per file, and the failure of one file reaches only as far as the catcher of the file that is pulling it in. If there is no catcher, the `ParseError` too falls into the halts channel.

#### Summary

- The file is parsed in full before it runs; not a single line of a file with a syntax error runs.
- An error thrown during execution keeps the earlier output: `fatal.php` printed one line, `broken.php` printed none; both exited with 255.
- This lesson's channel is halts: a syntax error cuts execution before it starts, and an error thrown during execution cuts it after it has started.
- On the built-in server, a file with a syntax error gives `500` with no body; an error thrown during execution leaves partial output with `200` after the body has begun.
- The `?>` and `<?=` tags mix code and template; in a file that carries only code, no closing tag is written, because an extra newline after it adds bytes to the output.
- The rule is per file: a syntax error in an included file is thrown, after the main file has run, as a `ParseError` that can be caught.

#### Next Step

It is now clear that the file started running and what is inside the tags. The Variables and Data Types lesson measures, inside the code, whether the type of a value changes without a signal: can a value lose its type?

### Variables and Data Types

Source: https://academia.sh/en/courses/php-fundamentals/variables-and-data-types

How the type of a value in PHP can change without a signal: assignment, the integer limit, floating-point numbers, casts, the undefined variable, and how type declarations behave under strict types mode.

The Syntax and Tags lesson measured that a file either never runs or is cut while running; in both cases there was a signal: an exit code, a status code, an exception. This lesson goes down to where the signal never arrives: the value runs, execution continues, but the type of the value may have changed. The question is whether the type changes without a signal.

In PHP a variable is a name that starts with `$` and needs no type declaration. The Programming Fundamentals course (M01/K02) established the concepts of variable and type; here only the way PHP treats those concepts is measured. This arrangement is called loose typing. To read the type, `get_debug_type()` is used: it gives names such as `int`, `float`, `string`, `bool`, `array` and `null`. The same method is followed in every measurement: the file sets up an error handler, the handler increases a counter at every notice, and the counter is printed at the end of the file. If the counter is zero, the language has said nothing.

#### Type Depends on the Value

In PHP the type is a property not of the variable but of the value it carries at that moment. The same name `$x` can carry an integer, a string, an array and a null one after another, and the language says nothing about it. The file below makes four assignments and reads the type after each one.

```php
<?php
// assignment.php — the same name carries four different types in a row, no signal arrives
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$x = 5;
echo get_debug_type($x), "\n";
$x = 'five';
echo get_debug_type($x), "\n";
$x = [1, 2];
echo get_debug_type($x), "\n";
$x = null;
echo get_debug_type($x), "\n";
echo 'notices: ', $notices, "\n";
```

```
int
string
array
null
notices: 0
```

**LE16.** Four assignments produced four different types and the notice counter stayed at zero. This is the most basic consequence of loose typing: to say the type of a value by looking at a variable, every assignment up to that line has to be traced. What is measured is not the freedom itself but that it is marked by no signal. The channel of this lesson is silent.

#### The Integer Limit

An `int` value has an upper limit, and the constant `PHP_INT_MAX` gives it. The value of the constant depends on the build and is not printed in this lesson; what is measured is what happens when the limit is exceeded. The file below adds one to the limit and reads the type before and after the result.

```php
<?php
// overflow.php — exceeding the integer limit silently changes the type
$warnings = 0;
set_error_handler(function (int $level) use (&$warnings): bool {
    $warnings++;
    return true;
});

$limit = PHP_INT_MAX;
$next = $limit + 1;
echo get_debug_type($limit), ' -> ', get_debug_type($next), "\n";
echo 'reported warnings: ', $warnings, "\n";
```

```
int -> float
reported warnings: 0
```

**LE17.** One more than the limit did not throw an error, did not report a warning and was not truncated: the value became a `float` and the operation continued. The line `reported warnings: 0` in the output shows that no signal arrived. The result is not wrong, it is of another kind: a value stored as a `float` can no longer tell all integers apart. The `integer overflow` row in the core table had placed this result in the silent channel; the measurement confirmed it.

#### Floating-Point Numbers and Division

A change of type does not happen only at the limit. When the division operator `/` divides two integers, the type of the result depends on whether the quotient comes out exact. Floating-point numbers, on the other hand, cannot store most decimal fractions exactly. The file below measures three things: the result type of division, whether the sum `0.1 + 0.2` equals `0.3`, and the type of the integer division `intdiv()`.

```php
<?php
// fraction.php — division changes the type, floating point does not store exactly
$divisions = [
    '6 / 3'        => 6 / 3,
    '7 / 2'        => 7 / 2,
    'intdiv(7, 2)' => intdiv(7, 2),
];
foreach ($divisions as $expression => $result) {
    printf("%-14s%-7s%s\n", $expression, get_debug_type($result), var_export($result, true));
}

$sum = 0.1 + 0.2;
echo '0.1 + 0.2 === 0.3: ', var_export($sum === 0.3, true), "\n";
echo 'difference within 1e-9: ', var_export(abs($sum - 0.3) < 1e-9, true), "\n";
```

```
6 / 3         int    2
7 / 2         float  3.5
intdiv(7, 2)  int    3
0.1 + 0.2 === 0.3: false
difference within 1e-9: true
```

**LE18.** `6 / 3` gave an `int` and `7 / 2` a `float`: the result type of the same operator changed with the values of the operands. The sum `0.1 + 0.2` did not come out strictly equal to `0.3`, but the difference between them is very small. Both are silent; using a threshold instead of equality in a floating-point comparison is the programmer's job. If exact division is wanted, there is `intdiv()`, whose type is always `int`.

#### Boolean Conversion

Which values count as false when a value is converted to a boolean in a condition or with `(bool)` is another face of the subject of types. The file below converts eleven values.

```php
<?php
// truthiness.php — which values count as false
$values = [
    '0'       => 0,
    '0.0'     => 0.0,
    "''"      => '',
    "'0'"     => '0',
    "'0.0'"   => '0.0',
    "' '"     => ' ',
    "'false'" => 'false',
    '[]'      => [],
    '[0]'     => [0],
    'null'    => null,
    '-1'      => -1,
];
foreach ($values as $name => $value) {
    printf("%-9s%s\n", $name, var_export((bool) $value, true));
}
```

```
0        false
0.0      false
''       false
'0'      false
'0.0'    true
' '      true
'false'  true
[]       false
[0]      true
null     false
-1       true
```

**LE19.** Zero, the empty string, `'0'`, the empty array and `null` counted as false; `'0.0'`, a single space, the string `'false'`, an array holding one element and `-1` counted as true. The difference between `'0'`, which counts as false, and `'0.0'`, which counts as true, shows that the conversion looks at the form of the value, not its meaning. None of them produces a notice.

#### Number Notations

The same integer can be written in several forms in source code: hexadecimal, binary, octal, and with underscores for readability. All of them produce the same type, `int`. The file below puts six notations side by side and prints their types and values; what is measured is whether the notation affects the type.

```php
<?php
// notation.php — number notations produce the same type
$notations = [
    '0x1F'      => 0x1F,
    '0b101'     => 0b101,
    '0o17'      => 0o17,
    '017'       => 017,
    '1_000_000' => 1_000_000,
    '1e3'       => 1e3,
];
foreach ($notations as $notation => $value) {
    printf("%-10s%-6s%s\n", $notation, get_debug_type($value), var_export($value, true));
}
```

```
0x1F      int   31
0b101     int   5
0o17      int   15
017       int   15
1_000_000 int   1000000
1e3       float 1000.0
```

**LE20.** Five notations gave `int`; only `1e3` became a `float`, because exponent notation always produces a floating-point number and its type does not change even when its value equals an integer. `017`, which starts with `0`, was read as octal, that is, it became `15`; someone who takes the leading zero for decoration silently produces a wrong value. This class of notation is also silent.

#### Null Checks

There are three ways to tell that a variable has no value, and the three give different results: `isset()`, `empty()` and `??`. To measure the differences, the same five values are given to all three. The fifth variable has never been defined; none of the checks produces a warning because of it, which is a result worth measuring in itself.

```php
<?php
// null_checks.php — isset, empty and ?? answer different questions
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$zero = 0;
$blank = '';
$none = null;
$filled = 'a';

printf("%-7s%-7s%-7s%s\n", 'name', 'isset', 'empty', '??');
foreach (['zero', 'blank', 'none', 'filled', 'never'] as $name) {
    $value = $$name ?? null;
    printf(
        "%-7s%-7s%-7s%s\n",
        $name,
        var_export(isset($$name), true),
        var_export(empty($$name), true),
        var_export($value, true)
    );
}
echo 'notices: ', $notices, "\n";
```

```
name   isset  empty  ??
zero   true   true   0
blank  true   true   ''
none   false  true   NULL
filled true   false  'a'
never  false  true   NULL
notices: 0
```

**LE21.** `isset()` answers only the question "defined and not `null`": `true` for `0` and the empty string, `false` for `null` and for undefined. `empty()` asks the opposite question and also counts `0` and the empty string as empty. `??` carries the value of the left side or `null`. The undefined `$never` produced no notice in any of the three checks; only a direct read produces the warning. Choosing the wrong check leads to logic errors such as counting `0` as "missing", and these errors are reported to no one.

#### Casts

The type can be changed at the programmer's wish just as it changes as the result of an operation. Writing a type name in parentheses converts the value to that type; this is called a cast. The file below runs six conversions and prints their results together with their types. The inputs include a partly numeric string, a string with no number at all, and a fractional number.

```php
<?php
// cast.php — casts change the result and give no signal
$casts = [
    '(int) "12abc"'   => (int) '12abc',
    '(int) "abc"'     => (int) 'abc',
    '(int) 3.99'      => (int) 3.99,
    '(bool) "0"'      => (bool) '0',
    '(string) false'  => (string) false,
    '(float) "1e3"'   => (float) '1e3',
];

foreach ($casts as $expression => $result) {
    printf("%-18s%-8s%s\n", $expression, get_debug_type($result), var_export($result, true));
}
```

```
(int) "12abc"     int     12
(int) "abc"       int     0
(int) 3.99        int     3
(bool) "0"        bool    false
(string) false    string  ''
(float) "1e3"     float   1000.0
```

**LE22.** The numeric prefix of the string was taken, the rest was thrown away, and nothing was said about it. `"12abc"` became `12`, `"abc"` became `0`, and `3.99` became `3` by truncation. There is no warning on any line of the output; a cast is a way of saying "I know what I am asking", and the language adds no signal to it. The programmer wrote the conversion, but the language does not say whether the result is lossless.

#### Undefined Variable

Reading a variable before any value has been assigned to it is a separate case. M09/K01 measured the question of what stands in for what was not written: there, a zero value took the place of an unassigned value. In PHP an undefined variable does not give a zero value: it gives the null value `null` and reports an `E_WARNING`. The file below prints only the level name in the error handler; the message text is not printed because it depends on the version.

```php
<?php
// undefined.php — an undefined variable gives null and reports E_WARNING
set_error_handler(function (int $level): bool {
    echo 'reported: ', $level === E_WARNING ? 'E_WARNING' : 'other', "\n";
    return true;
});

$result = $missing;
echo 'value: ', var_export($result, true), "\n";
echo 'type: ', get_debug_type($result), "\n";
```

```
reported: E_WARNING
value: NULL
type: null
```

**LE23.** The value is `null`, the type is `null` and the operation continued; but this time a warning arrived. The channel of the undefined variable is warning: it is heard, it does not halt. A variable that carries `null` instead of `0` or the empty string leaves a path open to another surprise in later operations; this leads to the conversion rules that are the subject of the Operators lesson.

#### Type Declarations

Loose typing can be limited by writing a type on a parameter. Writing `int $n` asks for the parameter to be an integer. The language meets this request in two ways: if the value fits, it accepts it; for one that does not fit, it either coerces or rejects it. The file below tests three inputs: a string that looks like an integer, a fractional number, and a string that carries a non-numeric suffix.

```php
<?php
// type.php — int parameter: '5' is coerced, 1.5 gives E_DEPRECATED, '5x' gives TypeError
set_error_handler(function (int $level): bool {
    echo 'reported: ', $level === E_DEPRECATED ? 'E_DEPRECATED' : 'other', "\n";
    return true;
});

function double_it(int $n): int
{
    return $n * 2;
}

foreach (['5', 1.5, '5x'] as $input) {
    try {
        $result = double_it($input);
        echo var_export($input, true), ' -> ', $result, "\n";
    } catch (TypeError $error) {
        echo var_export($input, true), ' -> ', get_class($error), "\n";
    }
}
```

```
'5' -> 10
reported: E_DEPRECATED
1.5 -> 2
'5x' -> TypeError
```

**LE24.** Three inputs were heard through three different channels. `'5'` silently became `5` and the result was `10`. Because `1.5` is fractional, `E_DEPRECATED` was reported and the value ran truncated. `'5x'` carries a non-numeric suffix, so it threw `TypeError`. A type declaration is therefore a rule, not a guarantee: the rule coerces the values inside its scope, rejects those outside it, and warns for those in between.

#### The Bounding Measurement

The mechanism that turns coercion off is the `declare(strict_types=1)` declaration, and the real limit of the measurement is whom it binds. **LE25.** Strict types mode applies to the caller's file, not to the file of the called function. A two-file setup shows this: two function files, one with the strict declaration and one without, carry the same function with an `int` parameter; two caller files send `'5'` to it.

```php
<?php
// function_loose.php — a function with an int parameter defined in a file without the mode
function double_loose(int $n): int
{
    return $n * 2;
}
```

```php
<?php
// function_strict.php — a function defined in a file that declares strict types mode
declare(strict_types=1);

function double_strict(int $n): int
{
    return $n * 2;
}
```

```php
<?php
// caller_strict.php — calls the function in the file without the mode from a strict file
declare(strict_types=1);
require 'function_loose.php';

try {
    echo double_loose('5'), "\n";
} catch (TypeError $error) {
    echo get_class($error), "\n";
}
```

```
TypeError
```

```php
<?php
// caller_loose.php — calls the function in the strict file from a file without the mode
require 'function_strict.php';

echo double_strict('5'), "\n";
```

```
10
```

When `'5'` was sent from the strict file to a function in a file without the mode, `TypeError` arrived; when the same value went from a file without the mode to a function declared strict, the output was `10`. The rule works in the file where the call is made, not in the file where the function is located. For this reason, looking at the parameter of a function does not tell how the call will behave: the same function coerces or rejects depending on which file it is called from.

#### Summary

- In PHP the type depends on the value: the same variable can carry four types in a row and no notice arrives; the channel of this lesson is silent.
- When the integer limit is exceeded the value becomes a `float`, the `/` operator gives an `int` or a `float` depending on whether the quotient is exact, and none of them produces a warning.
- Casts take the numeric prefix of a string such as `"12abc"`, throw the rest away and do not announce it.
- An undefined variable gives `null`, not a zero value, and reports an `E_WARNING`.
- A type declaration gives coercion for `'5'`, `E_DEPRECATED` for `1.5` and `TypeError` for `'5x'` on an `int` parameter.
- `declare(strict_types=1)` binds the caller's file: `'5'` is rejected from a strict file, and the same value is coerced to a strict function from a file without the mode.

#### Next Step

It was seen that the type changes silently in assignment, in division and in casts. The Operators lesson asks the same question of operators: what do arithmetic and comparison operators say when they bring two different types together?

### Operators

Source: https://academia.sh/en/courses/php-fundamentals/operators

What PHP operators say when they bring two types together: arithmetic with numeric strings, loose and strict equality, concatenation and precedence, division by zero, and the null coalescing operator silencing a warning.

The Variables and Data Types lesson measured that the type of a value can change in assignment, in division and in casts without a signal. This lesson asks the same question of operators: what does the language say when an operator brings two different types together, or when its operand is not suitable for the operator? The answer varies from operator to operator, and the channel of this lesson is warning: the operation usually continues, but it carries a notice with it.

In JavaScript the same question was measured in M06/K01: there, coercion and equality worked by their own rules. PHP's rules are separate from those, and the measurement keeps the method the same: each file sets up an error handler, the handler prints the level of the notice by name, and the message text is not printed, because the message depends on the version.

#### Arithmetic with Numeric Strings

PHP can use a string in arithmetic: if the string reads like a number, it is converted to a number. There are three cases. The string is entirely a number, the string starts with a number and continues with other characters, or the string has no number at all. The file below adds each of the three to `1` and prints the result of each or the exception it throws.

```php
<?php
// arithmetic.php — numeric string: integer is silent, prefix warns, non-numeric is TypeError
set_error_handler(function (int $level): bool {
    echo 'reported: ', $level === E_WARNING ? 'E_WARNING' : 'other', "\n";
    return true;
});

foreach (['5', '5 apples', 'apples'] as $input) {
    try {
        $result = $input + 1;
        echo var_export($input, true), ' + 1 = ', var_export($result, true), "\n";
    } catch (TypeError $error) {
        echo var_export($input, true), ' + 1 -> ', get_class($error), "\n";
    }
}
```

```
'5' + 1 = 6
reported: E_WARNING
'5 apples' + 1 = 6
'apples' + 1 -> TypeError
```

**LE26.** Three inputs gave three different results. The entirely numeric string was silently converted to a number. The string that starts with a number was processed with its numeric prefix and reported an `E_WARNING`; the operation still finished. The string that carries no number threw a `TypeError` without doing the addition. This lesson names its channel warning, because the middle case both produces a result and carries a notice; but the three cases show three different channels side by side in a single operator.

#### Loose and Strict Equality

`==` tries to bring two values to a common type before comparing them; `===` looks at both the type and the value. The two can give different answers for the same pair, and no signal ever arrives. The file below compares eight pairs with the two operators. The pairs were chosen to cover most of the common misunderstandings: numeric strings, the empty string and `null`, zero and a string, the empty array and `false`.

```php
<?php
// equality.php — loose and strict equality answer differently for the same pair
$pairs = [
    ["'1e3' and '1000'", '1e3', '1000'],
    ["'abc' and 0", 'abc', 0],
    ["'0' and false", '0', false],
    ["'' and null", '', null],
    ["'1' and '01'", '1', '01'],
    ["100 and '1e2'", 100, '1e2'],
    ['[] and false', [], false],
    ['null and 0', null, 0],
];
printf("%-18s%-7s%s\n", 'pair', '==', '===');
foreach ($pairs as [$name, $a, $b]) {
    printf("%-18s%-7s%s\n", $name, var_export($a == $b, true), var_export($a === $b, true));
}
```

```
pair              ==     ===
'1e3' and '1000'  true   false
'abc' and 0       false  false
'0' and false     true   false
'' and null       true   false
'1' and '01'      true   false
100 and '1e2'     true   false
[] and false      true   false
null and 0        true   false
```

**LE27.** `'1e3' == '1000'` came out true: because both strings are numeric they were compared as numbers, and `1e3` means a thousand. `'abc' == 0` came out false: a non-numeric string is not counted as equal to `0` when compared with a number. `'0' == false`, `'' == null`, `[] == false` and `null == 0` came out true. Strict equality gave true for none of the eight pairs, because in all eight the types are different. This table shows that `==` has no signal but that its result depends on the form of the value.

#### Comparison, Concatenation and Precedence

The `<=>` operator (the spaceship operator) compares two values and gives `-1`, `0` or `1`. The `.` operator concatenates strings. `and` and `&&` do the same logic with different precedence. The file below runs four expressions: numeric strings with `<=>`, a non-numeric string with `<=>`, a mix of `+` and `.`, and `and` with assignment.

```php
<?php
// precedence.php — comparison and precedence change the result
echo "'10' <=> '9':  ", '10' <=> '9', "\n";
echo "'10' <=> '9a': ", '10' <=> '9a', "\n";
echo "1 + 2 . '3':   ", 1 + 2 . '3', "\n";
echo "'1' . 2 + 3:   ", '1' . 2 + 3, "\n";

$a = true and false;
$b = (true and false);
echo 'a: ', var_export($a, true), ' b: ', var_export($b, true), "\n";
```

```
'10' <=> '9':  1
'10' <=> '9a': -1
1 + 2 . '3':   33
'1' . 2 + 3:   15
a: true b: false
```

**LE28.** Two numeric strings were compared as numbers: `'10'` is greater than `'9'`. Because `'9a'` is not numeric, the comparison was made as strings and `'10'` came out smaller. In the mix of `+` and `.` the addition was done first: the expression `1 + 2 . '3'` gave `'33'` and the expression `'1' . 2 + 3` gave `'15'`. The last two lines are the most insidious: because `and` is processed after assignment, in the expression `$a = true and false` the value of `$a` stayed true, while the parenthesized `$b` became false. None of them produced a notice.

#### Increment and Decrement

The `++` and `--` operators do the expected job on numbers, but their behavior diverges when the operand is a string or `null`. Strings are incremented alphabetically, as in Perl; when `null` is incremented it becomes `1`, and when decremented it stays `null`. The file below runs six cases and prints the result and its type, and the handler reports the level.

```php
<?php
// increment.php — ++ and -- behave differently on strings and null
set_error_handler(function (int $level): bool {
    echo $level === E_DEPRECATED ? "  [E_DEPRECATED]\n" : ($level === E_WARNING ? "  [E_WARNING]\n" : "  [other]\n");
    return true;
});

$cases = ['5', 'a', 'z', 'Az', 'a9', ''];
foreach ($cases as $start) {
    $x = $start;
    $x++;
    printf("%-6s++ -> %-7s%s\n", var_export($start, true), var_export($x, true), get_debug_type($x));
}

$up = null;
$up++;
$down = null;
$down--;
echo 'null++ -> ', var_export($up, true), ' null-- -> ', var_export($down, true), "\n";
```

```
'5'   ++ -> 6      int
  [E_DEPRECATED]
'a'   ++ -> 'b'    string
  [E_DEPRECATED]
'z'   ++ -> 'aa'   string
  [E_DEPRECATED]
'Az'  ++ -> 'Ba'   string
  [E_DEPRECATED]
'a9'  ++ -> 'b0'   string
  [E_DEPRECATED]
''    ++ -> '1'    string
  [E_WARNING]
null++ -> 1 null-- -> NULL
```

**LE29.** `'5'` became an integer when incremented and stayed silent. `'a'` became `'b'`, `'z'` became `'aa'`, `'Az'` became `'Ba'`, `'a9'` became `'b0'`: on non-numeric strings, increment is an alphabetical rule, and all five strings reported an `E_DEPRECATED`; this notice was extended to all non-numeric strings with PHP 8.5. `null` gave `1` when incremented and stayed silent; when decremented it stayed `null` and reported an `E_WARNING`. The same operator gave a silent, a deprecated or a warned result depending on the type of the operand.

#### Remainder and Power

The `%` operator gives the remainder, but its sign is the sign of the dividend; operands are converted to integers and the fraction is thrown away. The `**` operator raises to a power, and if the result exceeds the `int` limit it becomes a `float`. The file below prints five expressions.

```php
<?php
// remainder.php — % takes its sign from the dividend, ** becomes float on overflow
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$expressions = [
    '-7 % 3'   => -7 % 3,
    '7 % -3'   => 7 % -3,
    '7.9 % 3'  => 7.9 % 3,
    '2 ** 10'  => 2 ** 10,
    '2 ** -1'  => 2 ** -1,
];
foreach ($expressions as $expression => $result) {
    printf("%-10s%-6s%s\n", $expression, get_debug_type($result), var_export($result, true));
}
echo '2 ** 200: ', get_debug_type(2 ** 200), "\n";
echo 'notices: ', $notices, "\n";
```

```
-7 % 3    int   -1
7 % -3    int   1
7.9 % 3   int   1
2 ** 10   int   1024
2 ** -1   float 0.5
2 ** 200: float
notices: 1
```

**LE30.** The expression `-7 % 3` gave `-1` and the expression `7 % -3` gave `1`: the sign comes from the dividend. In the expression `7.9 % 3`, `7.9` was first truncated to `7`, the result was `1`, and one notice was counted; the implicit conversion of a fractional number to an integer gives a loss warning. `2 ** 10` is an `int` and `2 ** -1` a `float`; `2 ** 200` overflowed and became a `float`, and the type changed without carrying a notice. The last line is where this lesson meets the Variables and Data Types lesson: exceeding the integer limit is silent here too.

#### Compound Assignment Operators

Compound assignment operators such as `+=`, `.=` and `??=` read the variable and write it again. If the variable is undefined, the reading part produces a warning; `??=` makes the read safe and assigns only if there is no value. The file below adds to an undefined counter and to an undefined string, and then does the same job with `??=`. The handler prints the level name; which notice came from which line is read from the order in the output.

```php
<?php
// compound.php — += and .= warn when reading an undefined variable, ??= does not
set_error_handler(function (int $level): bool {
    echo $level === E_WARNING ? "  [E_WARNING]\n" : "  [other]\n";
    return true;
});

echo "+= undefined:\n";
$counter += 5;
echo '  counter: ', var_export($counter, true), "\n";

echo ".= undefined:\n";
$text .= 'a';
echo '  text: ', var_export($text, true), "\n";

echo "??= undefined:\n";
$timeout ??= 30;
echo '  timeout: ', var_export($timeout, true), "\n";

echo "??= defined:\n";
$timeout ??= 99;
echo '  timeout: ', var_export($timeout, true), "\n";
```

```
+= undefined:
  [E_WARNING]
  counter: 5
.= undefined:
  [E_WARNING]
  text: 'a'
??= undefined:
  timeout: 30
??= defined:
  timeout: 30
```

**LE31.** `+=` and `.=` each reported an `E_WARNING` while reading the undefined variable, but the operation continued: the counter became `5` and the string became `'a'`. On the first call `??=` assigned the variable and produced no notice; on the second call, because the variable was defined, it did nothing and `30` remained. This is the honest way to avoid a warning: expressing the absence of the variable as a decision names the need instead of silencing the warning.

#### Division by Zero

Division by zero is the first halting behavior of this lesson. The `/` and `%` operators and `intdiv()` throw a `DivisionByZeroError` when dividing by zero; `fdiv()` does the operation without an error and gives floating-point infinity. The file below tests four calls. The result is computed and printed separately, because an `echo` with commas leaves partial output if it is cut in the middle of the operation.

```php
<?php
// zero.php — division by zero halts, fdiv gives infinity
$zero = 0;
$calls = [
    '1 / 0'         => fn() => 1 / $zero,
    '1 % 0'         => fn() => 1 % $zero,
    'intdiv(1, 0)'  => fn() => intdiv(1, $zero),
    'fdiv(1, 0)'    => fn() => fdiv(1, $zero),
];
foreach ($calls as $name => $call) {
    try {
        $result = $call();
        $text = var_export($result, true);
    } catch (DivisionByZeroError $error) {
        $text = get_class($error);
    }
    printf("%-14s%s\n", $name, $text);
}
```

```
1 / 0         DivisionByZeroError
1 % 0         DivisionByZeroError
intdiv(1, 0)  DivisionByZeroError
fdiv(1, 0)    INF
```

**LE32.** Three operations threw `DivisionByZeroError`, while `fdiv()` returned `INF`. This shows the limit of this lesson's channel: the language has a kind of error that is not content with a warning. If the error is not caught, execution is cut as in the Syntax and Tags lesson; if it is caught, the program continues. Infinity itself is a silent value and can turn into `NAN` in later operations.

#### The Bounding Measurement

There is one operator that can silence all the notices in this lesson: the null coalescing operator `??`. When an undefined array key or variable is read, a warning is reported; if the same read is done with `??`, no notice is produced at all and that information is lost. The file below reads the same missing key in three ways, and the error handler counts every notice.

```php
<?php
// silencing.php — ?? silences the warning and hides the missing key
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$settings = ['timeout' => 30];

$a = $settings['timeot'];
echo 'direct read:    ', var_export($a, true), ' notices: ', $notices, "\n";

$b = $settings['timeot'] ?? 0;
echo 'read with ??:   ', var_export($b, true), ' notices: ', $notices, "\n";

$c = $settings['timeout'] ?? 0;
echo 'correct key:    ', var_export($c, true), ' notices: ', $notices, "\n";
```

```
direct read:    NULL notices: 1
read with ??:   0 notices: 1
correct key:    30 notices: 1
```

**LE33.** The direct read gave `null` and produced a notice. Reading the misspelled key with `??` produced no notice and returned `0`: the typo became visible as a valid default value. The third read found the correct key and gave `30`; the counter stayed at one again. This is not a bug but the limit of the measurement: `??` silences the warning, so a channel of the warning kind turns into the silent channel behind an operator. If a default value is the desired behavior, `??` is right; if catching the typo is the desired behavior, it should not be used.

#### Summary

- The channel of operators is warning: a string that starts with a number reports an `E_WARNING` and the operation continues; an entirely numeric string is silent; a string with no number throws a `TypeError`.
- `==` compares by bringing two values to a common type, `===` also checks the type; for none of the eight pairs did both come out true, and no comparison produced a notice.
- `+` is processed before the `.` operator, and `and` runs after assignment; the expression `$a = true and false` assigns true to `$a`.
- `/`, `%` and `intdiv()` throw `DivisionByZeroError` when dividing by zero; `fdiv()` returns `INF`.
- `??` silences the warning for an undefined key and variable; a typo thus turns into a silent default.

#### Next Step

It was seen what operators report and what they silence when they bring types together. The Control Flow lesson asks the same question of branches: what does the language do when no arm of a condition matches, that is, when an unmatched branch is left?

### Control Flow

Source: https://academia.sh/en/courses/php-fundamentals/control-flow

How unmatched branches behave in PHP: the difference between switch and match, loose and strict comparison, the lifetime of loop variables, the foreach and reference trap, and a default arm in match turning a halt into silence.

The Operators lesson measured what operators that bring types together report and what they silence. This lesson asks the same question of branches: when none of a condition, a case or a `match` arm matches, that is, when an unmatched branch is left, what does the language do? The answer differs between the two statements, and the channel of this lesson is halts: at least one statement cuts execution if there is no match.

PHP has `if`, `elseif` and `else` for conditional branching, `switch` and `match` for multiple selection, and `for`, `while`, `do while` and `foreach` for repetition. The logic of these structures was established in the Programming Fundamentals course (M01/K02); here only PHP's matching and exit behavior is measured. Each file sets up an error handler, and the handler makes a signal visible if one arrives.

#### The Unmatched Branch: switch and match

`switch` compares a value with the `case` labels in order, and if none matches and there is no `default`, it passes on without doing anything. `match` does the same job as an expression and returns a value; if no arm matches, it throws an `UnhandledMatchError`. The file below passes three values through the two statements. The `$result` variable starts each round with a previous value; what that value is in the unmatched branch is what is measured.

```php
<?php
// unmatched.php — switch passes silently, match halts
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

foreach ([1, 2, 3] as $code) {
    $result = 'previous';
    switch ($code) {
        case 1:
            $result = 'one';
            break;
        case 2:
            $result = 'two';
            break;
    }
    echo "switch($code): $result\n";

    $result = 'previous';
    try {
        $result = match ($code) {
            1 => 'one',
            2 => 'two',
        };
        echo "match($code):  $result\n";
    } catch (UnhandledMatchError $error) {
        echo "match($code):  ", get_class($error), ' (result: ', $result, ")\n";
    }
}
echo 'notices: ', $notices, "\n";
```

```
switch(1): one
match(1):  one
switch(2): two
match(2):  two
switch(3): previous
match(3):  UnhandledMatchError (result: previous)
notices: 0
```

**LE34.** In the third round `switch` did nothing: the variable stayed at its previous value and the notice counter did not rise. On the same input `match` threw `UnhandledMatchError`, and because the assignment did not take place, `$result` again stayed at its previous value; the difference is that in this case the language no longer stays silent. The `match` expression defines a value that does not match as an error; `switch` counts it as a normal execution path.

#### Loose and Strict Comparison

The second difference between the two is the tightness of the comparison. `switch` compares labels with `==`, that is, loose equality; `match` compares arms with `===`, that is, strict equality. The same input can go to different arms in the two statements. The file below gives five inputs to the two statements; the label and arm lists are the same.

```php
<?php
// comparison.php — switch compares loosely, match compares strictly
$inputs = [
    ["'1'", '1'],
    ['null', null],
    ["''", ''],
    ['0', 0],
    ["'a'", 'a'],
];
printf("%-6s%-13s%s\n", 'input', 'switch', 'match');
foreach ($inputs as [$name, $input]) {
    switch ($input) {
        case 1:
            $s = 'case 1';
            break;
        case false:
            $s = 'case false';
            break;
        case 'a':
            $s = "case 'a'";
            break;
        default:
            $s = 'default';
    }
    $m = match ($input) {
        1 => 'arm 1',
        false => 'arm false',
        'a' => "arm 'a'",
        default => 'default',
    };
    printf("%-6s%-13s%s\n", $name, $s, $m);
}
```

```
input switch       match
'1'   case 1       default
null  case false   default
''    case false   default
0     case false   default
'a'   case 'a'     arm 'a'
```

**LE35.** The string `'1'` went to `case 1` in `switch`, while in `match` it did not fit the `1 =>` arm and fell to `default`. `null`, the empty string and `0` went to `case false` in `switch`, because in loose equality they are equal to `false`; in `match` none of them fit the `false =>` arm. Only `'a'` went to the same place in both statements. This is the reflection in control flow of the equality table in the Operators lesson: `switch` inherits all the surprises of loose equality without producing a notice.

#### Exits in Nested Loops

`break` ends a loop, `continue` moves to the next round, and both can take a number: `break 2` leaves two nested loops at once, `continue 2` moves to the next round of the outer loop. The language does not accept this number outside a valid level limit, but what the number skips is a detail the programmer has to read. The file below runs three nested cases and also prints the value of the loop variables after the loops end.

```php
<?php
// loop_exit.php — break 2 and continue 2 affect the outer loop
$pairs = [];
for ($i = 1; $i <= 3; $i++) {
    for ($j = 1; $j <= 3; $j++) {
        if ($j === 2) {
            continue;
        }
        if ($i === 2) {
            continue 2;
        }
        if ($i === 3 && $j === 3) {
            break 2;
        }
        $pairs[] = $i . $j;
    }
}
echo implode(',', $pairs), "\n";
echo "i: $i j: $j\n";
```

```
11,13,31
i: 3 j: 3
```

**LE36.** The three pairs in the output show the effect of each exit: when `j` equals two the inner round was skipped, when `i` equals two the rest of the outer round was skipped, and when `i` is three and `j` is three both loops ended at once. The last line shows that in PHP a loop variable lives on after the loop ends: `i` and `j` are still defined and keep the values they had at the moment of exit. The scope of a loop variable is not the loop but the function that contains it; this can leave residue for code that later uses the same name.

#### foreach and the Reference Trap

`foreach` copies the elements of an array one by one into a variable; if the copy is changed, the array is not affected. If the variable is bound as a reference with `&`, the array changes, and after the loop ends this reference stays alive. A second `foreach` that reuses the same name without clearing the reference overwrites the last element of the array. The file below measures three cases.

```php
<?php
// reference.php — a foreach reference lives on after the loop
$a = [1, 2, 3];
foreach ($a as $v) {
    $v *= 10;
}
echo 'copy:           ', implode(',', $a), "\n";

$b = [1, 2, 3];
foreach ($b as &$w) {
    $w *= 10;
}
foreach ($b as $w) {
}
echo 'reference:      ', implode(',', $b), "\n";

$c = [1, 2, 3];
foreach ($c as &$x) {
    $x *= 10;
}
unset($x);
foreach ($c as $x) {
}
echo 'with unset:     ', implode(',', $c), "\n";
```

```
copy:           1,2,3
reference:      10,20,20
with unset:     10,20,30
```

**LE37.** The `foreach` that ran with a copy did not change the array. The one that ran with a reference multiplied every element by ten. Then the second loop, which used the same name as a copy, silently overwrote the last element of the array because the reference was still bound to the last element, and `10,20,20` came out. `unset($x)` cut the reference and `10,20,30` remained. This behavior is silent: no notice, no exception, only a wrong array.

#### Assignment Inside a Condition

A difference of a single character turns a condition into an assignment: `=` and `==`. PHP does not count this as a syntax error, because an assignment is an expression and its value is the assigned value. The condition decides according to that value. The file below writes the same condition in three forms; the error handler keeps a counter.

```php
<?php
// condition_assignment.php — inside if, = assigns and its value counts as the condition
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$state = 'empty';
if ($state = 'full') {
    echo "assignment: branch true, state: $state\n";
}

$state = 'empty';
if ($state == 'full') {
    echo "comparison: branch true\n";
} else {
    echo "comparison: branch false, state: $state\n";
}

$value = 5;
if ($value = 0) {
    echo "zero assignment: branch true\n";
} else {
    echo "zero assignment: branch false, value: $value\n";
}
echo 'notices: ', $notices, "\n";
```

```
assignment: branch true, state: full
comparison: branch false, state: empty
zero assignment: branch false, value: 0
notices: 0
```

**LE38.** When `'full'` was assigned the condition came out true and `$state` changed; the comparison kept the value. When `0` was assigned the condition came out false and `$value` dropped from five to zero. In all three cases the counter stayed at zero: the language counts assignment inside a condition as legitimate and gives no signal. The error is understood only from the fact that the value of the variable has changed after the condition.

#### How Many Times a Loop Runs

The number of rounds a loop makes can be worked out from the condition, but in three cases expectation and reality diverge. `do while` tests the condition after the round, so even with a false condition it runs once. A floating-point counter can make one round more than expected when it reaches `1` in steps of `0.1`. Adding an element to the array during the loop does not change the number of iterations of `foreach`, because `foreach` walks the array as it was at the start. The file below counts the three.

```php
<?php
// iteration_count.php — do while runs once, a floating-point counter runs extra
$count = 0;
do {
    $count++;
} while (false);
echo "do while:    $count\n";

$count = 0;
for ($x = 0; $x < 1; $x += 0.1) {
    $count++;
}
echo "for 0.1 step: $count\n";

$items = [1, 2];
$count = 0;
foreach ($items as $item) {
    $items[] = $item * 10;
    $count++;
}
echo "foreach:      $count iterations, array: ", count($items), " elements\n";
```

```
do while:    1
for 0.1 step: 11
foreach:      2 iterations, array: 4 elements
```

**LE39.** `do while` made one round even with a false condition. The floating-point counter made eleven rounds, not ten, because at the tenth addition the counter was not exactly `1` but stayed just below `1`; this is the loop side of the `0.1 + 0.2` measurement in the Variables and Data Types lesson. `foreach` made two rounds and the array grew to four elements: the array it walked is the copy from the first moment. All three are silent.

#### foreach and a Value That Is Not an Array

`foreach` expects an array or an object. If another value is given, the loop does not run at all and the language reports an `E_WARNING`; that is, unlike the channel of this lesson, this case does not halt but is not silent either. The file below gives `null`, a string and an empty array to the same loop.

```php
<?php
// not_an_array.php — foreach warns on a value that is not an array and does not run
set_error_handler(function (int $level): bool {
    echo $level === E_WARNING ? "  [E_WARNING]\n" : "  [other]\n";
    return true;
});

$inputs = [
    'null'      => null,
    "'abc'"     => 'abc',
    '[]'        => [],
    '[1, 2]'    => [1, 2],
];
foreach ($inputs as $name => $input) {
    $count = 0;
    foreach ($input as $item) {
        $count++;
    }
    echo "$name: $count iterations\n";
}
```

```
  [E_WARNING]
null: 0 iterations
  [E_WARNING]
'abc': 0 iterations
[]: 0 iterations
[1, 2]: 2 iterations
```

**LE40.** `null` and the string did not enter the loop and each reported an `E_WARNING`; the empty array and the two-element array ran without a notice. This difference between the empty array and `null` turns what a function returns for "no result" into a contract: returning `null` and giving it to `foreach` produces a warning on every call.

#### An Array That Changes During the Loop

When `foreach` works by value, it walks the array as it was at the start; even if the array is enlarged inside the loop, the number of rounds does not change. `for`, on the other hand, recomputes the condition every round; if the condition is `count()`, the rounds lengthen as the array grows. The file below counts the same enlargement in two loops.

```php
<?php
// growth.php — foreach walks the starting array, for recomputes its condition every round
$a = [1, 2, 3];
$rounds_a = 0;
foreach ($a as $v) {
    if ($rounds_a < 5) {
        $a[] = $v;
    }
    $rounds_a++;
}
echo 'foreach: ', $rounds_a, ' iterations, ', count($a), " elements\n";

$b = [1, 2, 3];
$rounds_b = 0;
for ($i = 0; $i < count($b); $i++) {
    if ($rounds_b < 3) {
        $b[] = 9;
    }
    $rounds_b++;
}
echo 'for:     ', $rounds_b, ' iterations, ', count($b), " elements\n";
```

```
foreach: 3 iterations, 6 elements
for:     6 iterations, 6 elements
```

**LE41.** `foreach` ran three rounds and the array grew to six elements: what it walked was the starting copy with three elements. `for` ran six rounds, because `count($b)` gave the new length every round. One does not see the growth, the other does; neither produces a notice. If the array a loop walks changes inside the loop, the number of iterations depends on which loop structure was chosen.

#### The Bounding Measurement

Because `match` halts on an unmatched input, an arm that carries `default` turns the halt into silence. This is the limit of this lesson's channel: the same statement passes from the halts channel to the silent channel when a single arm is added. The file below runs two functions, one with a `default` arm and one without, on the same inputs. The result is computed first and printed afterward.

```php
<?php
// fallback.php — a default arm turns the halt of match into silence
function name_strict(int $code): string
{
    return match ($code) {
        1 => 'one',
        2 => 'two',
    };
}

function name_default(int $code): string
{
    return match ($code) {
        1 => 'one',
        2 => 'two',
        default => 'unknown',
    };
}

foreach ([1, 7] as $code) {
    try {
        $strict = name_strict($code);
    } catch (UnhandledMatchError $error) {
        $strict = get_class($error);
    }
    $default = name_default($code);
    printf("%d  strict: %-20s default: %s\n", $code, $strict, $default);
}
```

```
1  strict: one                  default: one
7  strict: UnhandledMatchError  default: unknown
```

**LE42.** On the input `1` both functions returned `'one'`. On the input `7` the function without a `default` arm threw `UnhandledMatchError`, while the function with one returned `'unknown'` and reported nothing. In this case `default` does two things at once: it silently accepts the input that does not match, and it hides from the program that the input did not match. Which choice is right depends on whether the unmatched input is expected or an error; the measurement shows only the result of the choice.

#### Summary

- The channel of control flow is halts: `match` throws `UnhandledMatchError` on an unmatched arm, while `switch` passes silently and leaves the variable at its previous value.
- `switch` labels compare with loose equality and `match` arms with strict equality: `'1'` goes to `case 1` in `switch` and falls to `default` in `match`.
- `break 2` and `continue 2` affect the outer loop in nested loops; loop variables stay defined after the loop.
- A `foreach` that works with a reference leaves the reference behind after the loop; without `unset`, a second loop with the same name overwrites the last element of the array.
- Adding a `default` arm to `match` turns the halt into silence and hides the input that did not match.

#### Next Step

It was seen what an unmatched branch does: `switch` passes, `match` halts. The Functions lesson asks the same question of calls: when a function is given more arguments than it expects, where does the surplus go?

### Functions

Source: https://academia.sh/en/courses/php-fundamentals/functions

Argument and return behavior in PHP functions: silent swallowing of extra arguments, errors for missing and unknown arguments, default values and static, passing by value and by reference, return types, and making the surplus visible with ...$x.

The Control Flow lesson measured that an unmatched branch passes silently in `switch` and halts execution in `match`. This lesson asks the same question of calls: when a function is given more arguments than it expects, where does the surplus go, and what happens when too few are given? The channel of this lesson is silent: an argument given in excess to a function the user wrote is ignored without producing any notice.

In PHP, a function declared with the `function` keyword returns a value with `return`; its parameters can carry types and default values. The concept of a function was established in the Programming Fundamentals course (M01/K02); here only how PHP behaves toward that concept is measured. The question of whether a change is visible to the caller and to others was measured separately for objects in M11/K03; this lesson mentions it only as far as needed for scalar and array values.

#### Extra Arguments

If a user function is given more arguments than it has parameters, PHP does not count the surplus as an error: the function still runs and the extra arguments are ignored. Built-in functions are strict on this. The file below gives three arguments to a two-parameter function and tries the same thing with the built-in `strlen()`; the error handler keeps a counter.

```php
<?php
// extra.php — a user function swallows the extra argument, a built-in rejects it
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

function add_two($a, $b)
{
    return $a + $b;
}

echo 'add_two(1, 2, 3):  ', add_two(1, 2, 3), "\n";

try {
    $length = strlen('abc', 'x');
    echo 'strlen(abc, x):    ', $length, "\n";
} catch (ArgumentCountError $error) {
    echo 'strlen(abc, x):    ', get_class($error), "\n";
}
echo 'notices: ', $notices, "\n";
```

```
add_two(1, 2, 3):  3
strlen(abc, x):    ArgumentCountError
notices: 0
```

**LE43.** The expression `add_two(1, 2, 3)` returned `3`, that is, the third argument took no part in the sum and nothing was said about it: the counter stayed at zero. The built-in `strlen()` answered the same surplus with an `ArgumentCountError`. This asymmetry is a design decision: not counting extra arguments as an error in user functions allows a function to later gain an additional optional parameter. The cost is that a typo in a call can be swallowed.

#### Missing and Unknown Arguments

Unlike the surplus, a missing argument is an error: if a required parameter is not filled, an `ArgumentCountError` is thrown. Besides passing arguments by position, PHP allows passing them by name (a named argument). The file below makes five calls: a missing one, a complete one, a named one, one with an unknown name, and one that renames a parameter already filled by position.

```php
<?php
// missing.php — ArgumentCountError rejects a missing argument, Error rejects an unknown name
function greet(string $name, string $title = 'mr'): string
{
    return $title . ' ' . $name;
}

$calls = [
    'greet()'                          => fn() => greet(),
    "greet('Ada')"                     => fn() => greet('Ada'),
    "greet(title: 'dr', name: 'Ada')"  => fn() => greet(title: 'dr', name: 'Ada'),
    "greet('Ada', surname: 'X')"       => fn() => greet('Ada', surname: 'X'),
    "greet('Ada', name: 'Ece')"        => fn() => greet('Ada', name: 'Ece'),
];
foreach ($calls as $label => $call) {
    try {
        $text = var_export($call(), true);
    } catch (Error $error) {
        $text = get_class($error);
    }
    printf("%-34s%s\n", $label, $text);
}
```

```
greet()                           ArgumentCountError
greet('Ada')                      'mr Ada'
greet(title: 'dr', name: 'Ada')   'dr Ada'
greet('Ada', surname: 'X')        Error
greet('Ada', name: 'Ece')         Error
```

**LE44.** The missing argument gave an `ArgumentCountError`. When `$title`, which has a default value, was omitted, `'mr Ada'` was produced. Named arguments worked independently of order: `'dr Ada'` came out although `title` was written first. The name `surname`, which is not defined in the function, and the `name` parameter already filled by position each threw an `Error`. So an extra positional argument is silent and an extra named argument is loud; naming gives the user a stricter contract than the swallowing of extra arguments.

#### Default Values and static

A default value is recomputed on every call; that is, an empty array written as the default of a parameter is a new empty array on every call and does not carry the elements added by the previous call. A variable declared with `static` inside a function, on the other hand, lives across calls. The file below puts the two behaviors side by side.

```php
<?php
// defaults.php — the default is renewed on every call, static lives across calls
function append(int $item, array $list = []): array
{
    $list[] = $item;
    return $list;
}

function count_calls(): int
{
    static $counter = 0;
    $counter++;
    return $counter;
}

echo 'append(1):     ', count(append(1)), "\n";
echo 'append(2):     ', count(append(2)), "\n";

count_calls();
count_calls();
echo 'count_calls(): ', count_calls(), "\n";
```

```
append(1):     1
append(2):     1
count_calls(): 3
```

**LE45.** `append` returned one element in both calls: the default empty array was renewed on every call. `count_calls` gave `3` on the third call: the `static` variable was not erased each time the function finished. The two show the same distinction from two directions: parameters and local variables belong to the call, a `static` variable belongs to the function. Neither produces a notice.

#### By Value and by Reference

PHP passes arguments by value by default: the function works on its own copy and does not touch the caller's variable. If a parameter is declared with `&`, the function changes the caller's variable. The file below makes the same change with two declarations and counts the caller's array.

```php
<?php
// by_value.php — value copies, reference changes the caller's variable
function by_value(array $list, int $number): void
{
    $list[] = 99;
    $number++;
}

function by_reference(array &$list, int &$number): void
{
    $list[] = 99;
    $number++;
}

$items = [1, 2];
$n = 10;

by_value($items, $n);
echo 'by_value:     ', count($items), ' elements, n: ', $n, "\n";

by_reference($items, $n);
echo 'by_reference: ', count($items), ' elements, n: ', $n, "\n";
```

```
by_value:     2 elements, n: 10
by_reference: 3 elements, n: 11
```

**LE46.** After the call passed by value, the array stayed at two elements and the number at ten: the function changed the copy. After the call passed by reference, the array had three elements and the number was eleven. The consequence of this distinction for objects was measured in M11/K03; what is observed here is that unless `&` is written, the language does not make a change visible to the caller. A reference parameter is read from the function's signature: there is no mark at the call.

#### Return Values

A function that has no `return` returns `null`. If a return type is declared, PHP coerces the returned value to that type, just as with parameters: the coercion rule in the Variables and Data Types lesson works here too. The file below shows return behavior in four cases.

```php
<?php
// returns.php — null without return, the return type coerces or rejects
function nothing()
{
}

function to_int(): int
{
    return '5';
}

function broken(): int
{
    return 'five';
}

function blank(): void
{
}

echo 'nothing(): ', var_export(nothing(), true), "\n";
echo 'blank():   ', var_export(blank(), true), "\n";

$result = to_int();
echo 'to_int():  ', var_export($result, true), ' ', get_debug_type($result), "\n";

try {
    broken();
} catch (TypeError $error) {
    echo 'broken():  ', get_class($error), "\n";
}
```

```
nothing(): NULL
blank():   NULL
to_int():  5 int
broken():  TypeError
```

**LE47.** The function without `return` returned `null`, and so did the one declared `void`. The function declared `: int` silently converted the string `'5'` to `5`, while `'five'` gave a `TypeError` because it could not be coerced. The return type therefore follows the same rule as the parameter type: a numeric string is coerced, otherwise it is rejected.

#### The Moment a Function Is Defined

PHP can know a function before a file starts executing, but not every function. A function declared unconditionally at the top level of the file is recognized while the file is read and can be called on a line before its declaration. A function declared inside an `if` block exists only when that block is executed. A call to a function that was never declared throws an `Error`. The file below measures the three cases with `function_exists()`.

```php
<?php
// definition.php — a top-level function is known beforehand, a conditional one is defined when it runs
echo 'before:       ', var_export(function_exists('top_level'), true), "\n";
echo 'conditional:  ', var_export(function_exists('conditional'), true), "\n";
echo 'call:         ', top_level(), "\n";

function top_level(): string
{
    return 'ran';
}

if (true) {
    function conditional(): string
    {
        return 'x';
    }
}
echo 'after:        ', var_export(function_exists('conditional'), true), "\n";

try {
    no_such_thing();
} catch (Error $error) {
    echo 'undefined:    ', get_class($error), "\n";
}
```

```
before:       true
conditional:  false
call:         ran
after:        true
undefined:    Error
```

**LE48.** `top_level`, declared unconditionally, existed before its declaration line and the call worked. `conditional`, declared conditionally, did not exist before the block was executed and did exist afterward. An undefined name gave an `Error`, so it belongs to the halts class. What is silent is that, depending on the line on which the call is made, the same name works in one place and not in another: the existence of a function depends on the structure of the file, and the existence of a conditional one depends on execution order.

#### Closures and Captured Variables

An anonymous function (closure) cannot reach a variable in the outer scope on its own; it has to capture it explicitly with `use`. `use ($x)` copies the value at declaration time, while `use (&$x)` binds the variable itself. The arrow function `fn` captures the outer variable by value at the moment it is created. The file below measures the three capture forms by calling them after the captured variable has been changed.

```php
<?php
// closure.php — use captures the value, & binds the variable, an arrow function takes the value at creation
$x = 1;
$by_value = function () use ($x) {
    return $x;
};
$by_reference = function () use (&$x) {
    return $x;
};
$arrow = fn() => $x;

$x = 2;
echo 'use ($x):  ', $by_value(), "\n";
echo 'use (&$x): ', $by_reference(), "\n";
echo 'fn:        ', $arrow(), "\n";

$increase = fn() => $x++;
$increase();
echo 'x after increment in fn: ', $x, "\n";
```

```
use ($x):  1
use (&$x): 2
fn:        1
x after increment in fn: 2
```

**LE49.** After `$x` was set to two, the two forms that capture by value saw the old value, `1`; only the one bound with `&` saw `2`. The increment inside the arrow function did not change the outer `$x`: the variable entered the arrow function as a copy. The distinction is the counterpart of passing by value and by reference in closures, and it produces no notice: code that picks the wrong form does not fail, it works with an old value.

#### A Parameter That Accepts Null

Being able to give `null` to a parameter is two separate things: the parameter's type accepting `null`, and the parameter being omittable. `?int $x` accepts null but is required; `?int $x = null` both accepts null and can be omitted. A union type (`int|string`), in loose mode, chooses by an order which type to coerce to. The file below measures seven calls in a table.

```php
<?php
// nullable.php — ?int is required, = null can be omitted, a union type picks the coercion order
function required(?int $x)
{
    return var_export($x, true);
}

function optional(?int $x = null)
{
    return var_export($x, true);
}

function union(int|string $x)
{
    return get_debug_type($x);
}

$calls = [
    'required(null)'  => fn() => required(null),
    'required()'      => fn() => required(),
    'optional()'      => fn() => optional(),
    "union('5')"      => fn() => union('5'),
    'union(5.0)'      => fn() => union(5.0),
    'union([])'       => fn() => union([]),
    'union(true)'     => fn() => union(true),
];
foreach ($calls as $label => $call) {
    try {
        $text = $call();
    } catch (Error $error) {
        $text = get_class($error);
    }
    printf("%-20s%s\n", $label, $text);
}
```

```
required(null)      NULL
required()          ArgumentCountError
optional()          NULL
union('5')          string
union(5.0)          int
union([])           TypeError
union(true)         int
```

**LE50.** `required(null)` worked but `required()` gave an `ArgumentCountError`: `?` provides null acceptance, not omittability. `optional()` returned `NULL`. In the union type a value with an exact match stayed as it was; where there was no exact match PHP followed an order and chose the result silently; and a value such as an array, which cannot be converted to any member, gave a `TypeError`. This is the counterpart in union types of the loose mode observation in the type declaration lesson.

#### A Called Function

A function given to another function as an argument (a callback) can be called by the caller with more arguments than its declaration expects. A user function swallows this surplus silently again; a built-in function does not. The file below runs `array_filter()` in the mode that sends two arguments with a single-parameter closure, and makes the same test with the built-in `strlen` through `array_map()`.

```php
<?php
// callback.php — a called user function swallows the extra argument, a built-in does not
$numbers = [1, 2, 3, 4];

$single = array_filter($numbers, fn($v) => $v > 2, ARRAY_FILTER_USE_BOTH);
echo 'single parameter: ', implode(',', $single), "\n";

$double = array_filter($numbers, fn($v, $k) => $k % 2 === 0, ARRAY_FILTER_USE_BOTH);
echo 'two parameters:   ', implode(',', $double), "\n";

try {
    $result = array_map('strlen', ['a'], ['b']);
    echo 'strlen: ', count($result), "\n";
} catch (ArgumentCountError $error) {
    echo 'strlen: ', get_class($error), "\n";
}
```

```
single parameter: 3,4
two parameters:   1,3
strlen: ArgumentCountError
```

**LE51.** The single-parameter closure, called with two arguments, ran without ever seeing the key and gave `3,4`. The two-parameter one used the key and selected the two elements with even indexes, `1,3`. `array_map()`, called with two arrays, sent two arguments to `strlen` and received an `ArgumentCountError`. The same extra argument was silent in one place and halting in another, depending on the kind of callback.

#### The Bounding Measurement

The limit of silent swallowing in a user function is that making the surplus visible is possible. There are two ways: `func_num_args()` says how many arguments were actually given inside the function, while the `...$x` syntax gathers all remaining arguments into an array, and the surplus shows up in the length of that array. The file below measures the ways with the same call.

```php
<?php
// visible.php — func_num_args and ...$x make the surplus visible
function add_two($a, $b)
{
    return $a + $b;
}

function count_args($a, $b)
{
    return func_num_args();
}

function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

function count_all(...$x): int
{
    return count($x);
}

echo 'add_two(1, 2, 3):   ', add_two(1, 2, 3), "\n";
echo 'count_args(1, 2, 3): ', count_args(1, 2, 3), "\n";
echo 'sum(1, 2, 3):       ', sum(1, 2, 3), "\n";
echo 'count_all(1, 2, 3): ', count_all(1, 2, 3), "\n";
```

```
add_two(1, 2, 3):   3
count_args(1, 2, 3): 3
sum(1, 2, 3):       6
count_all(1, 2, 3): 3
```

**LE52.** `add_two` swallowed the third argument again and gave `3`. `func_num_args()` counted `3` in the same call: the swallowed argument can be measured from inside the function. The function written with `...$numbers` added all three numbers and gave `6`; `...$x` delivered the surplus as an array and `count` said `3`. The silent channel is therefore not unavoidable: the author of a function can choose a signature in which the surplus will be visible.

#### Summary

- The channel of functions is silent: an extra positional argument given to a user function is ignored without a notice, while a built-in function throws an `ArgumentCountError`.
- A missing required argument gives an `ArgumentCountError`, and an unknown or colliding named argument gives an `Error`; naming is stricter than position.
- A default value is renewed on every call, and a `static` variable lives across calls.
- Arguments are passed by value; the caller's variable changes only if `&` is written in the signature.
- A function without `return` returns `null`; a return type coerces a numeric string and rejects one that is not.
- `func_num_args()` and `...$x` make the swallowed surplus visible.

#### Next Step

Where the extra argument goes was seen: it was ignored or gathered into an array. The Indexed and Associative Arrays lesson looks at that array: when two arrays are merged, how do the keys combine for elements that carry the same key?

## Arrays and Strings

A single ordered map used as both list and dictionary, key normalization, missing keys in nested arrays, how array functions report what they cannot find, and string handling working over bytes.

### Indexed and Associative Arrays

Source: https://academia.sh/en/courses/php-fundamentals/indexed-and-associative-arrays

One array structure used as both a list and a dictionary; the keys '1', 1 and true collapsing into a single key; a decimal key being truncated; normalization applying only to canonical decimal integer strings.

The Functions lesson measured an extra argument being discarded without any signal: `add_two(1, 2, 3)` produced `3` and no word of it reached the caller. This lesson asks whether the same silence appears in a data structure: when the same array receives keys written in different forms and they run together, who says so? The course's measuring question does not change, which is which channel the problem is heard through, and the channel names are only used in this lesson: **silent**, **return**, **warning**, **halts**.

In PHP, `array` is a single structure that serves two uses. An array whose keys are consecutive integers reads as an indexed array, and an array whose keys are strings reads as an associative array. There is no type difference between them; the difference lies only in what the keys are. The variable and type concepts built in the Programming Fundamentals course (M01/K02) apply here as they stand. What is new is that a key is not stored as written; it passes through a rule first.

#### Two Uses in One Structure

The file below uses the same structure in three ways: as a list, as a dictionary, and as a mixture of the two. The keys of each array are printed side by side with `array_keys`.

```php
<?php
// arrays.php — the same structure serves as both a list and a dictionary
$items = ['apple', 'pear', 'quince'];
$items[] = 'pomegranate';

$person = ['name' => 'Ada', 'age' => 36];
$person['city'] = 'Izmir';

$mixed = ['one', 'two', 'special' => 'x'];
$mixed[] = 'three';

$ordered = [3 => 'c', 1 => 'a'];
$ordered[] = 'd';

$removed = ['x', 'y', 'z'];
unset($removed[1]);
$removed[] = 'w';

echo 'items: ', implode(',', array_keys($items)), "\n";
echo 'person: ', implode(',', array_keys($person)), "\n";
echo 'mixed: ', implode(',', array_keys($mixed)), "\n";
echo 'ordered: ', implode(',', array_keys($ordered)), "\n";
echo 'removed: ', implode(',', array_keys($removed)), "\n";
echo 'is list: ', var_export(array_is_list($items), true), ' ',
    var_export(array_is_list($person), true), ' ',
    var_export(array_is_list($removed), true), "\n";
```

```
items: 0,1,2,3
person: name,age,city
mixed: 0,1,special,2
ordered: 3,1,4
removed: 0,2,3
is list: true false false
```

Appending with square brackets adds an element without a key, and PHP gives it the next integer key: in the mixed array, the value `'three'` added after the `special` key received the key `2`. The `ordered` line shows that order depends on insertion and not on the key: the key `3` comes first because it was written first, followed by `1` and the appended `4`. The `removed` line shows that keys are not renumbered when an element is deleted, which is why `array_is_list` returns `false`. An array that looks indexed can stop being indexed after an `unset`, and nothing reports it.

**AS1.** In this lesson, "order" means the insertion order of the elements; `foreach` walks over the array in that order. The run above shows that order is independent of key values, and any job that needs numeric order sorts explicitly. Whether an array is copied when it is assigned to another variable is not the question of this lesson: that is the "who sees this change" question measured in M11/K03, and it is not asked here.

#### Key Normalization

An array key can only be an `int` or a `string`. A key written with any other type is normalized to one of these two: a `true` key becomes `1`, a decimal number is truncated to an integer, and a string in decimal integer form becomes an integer. That is the whole definition; what matters is that the conversion is invisible at the level of the written code. The file below pulls in `channel.php`; the channel classifier is the shared helper of the course, and its cross-section is used in this lesson.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

```php
<?php
// normalization.php — differently written keys collapsing into one key
require 'channel.php';

$observations = [
    'three forms, one key' => fn() => ['1' => 'a', 1 => 'b', true => 'c'],
    'decimal key'          => function () {
        $key = 1.7;
        return [$key => 'x'];
    },
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-22s%-9s%s\n", $name, $channel, $evidence);
}

$array = ['1' => 'a', 1 => 'b', true => 'c'];
echo 'remaining key: ', array_key_first($array), ', value: ', $array[1], "\n";
```

```
three forms, one key  silent   result array(1)
decimal key           warning  E_DEPRECATED, result array(1)
remaining key: 1, value: c
```

The first line is the course's `key normalization` observation, and it belongs to the **silent** channel. The keys `'1'`, `1` and `true` are three separate spellings; all three landed on the key `1`, one element remained in the array, and the value of that element is the last one written, `'c'`. `listen()` asked its questions in order: nothing was thrown, no warning was reported, and the result is neither `false` nor `null`; silence is what remains. The problem can be seen only by looking at the result. Someone who expected `array(3)` here instead of `array(1)` heard nothing about the two missing elements. This is the same channel as the discarded extra argument; the difference is that what is discarded here is a value, and the key overwrites it.

The second line shows another face of the same mechanism: the key `1.7` was truncated to `1`, and this time an error level was reported. `E_DEPRECATED` is the deprecation level, and execution continues; the channel is therefore **warning**. The keys `true` and `'1'` reach `1` without losing a value, whereas `1.7` drops its fractional part. The limit of the silence is drawn by information loss: a conversion that loses information is announced, and a conversion that appears lossless is not.

**AS2.** In this lesson `E_DEPRECATED` is recorded under the `warning` channel, because the definition in the core asks only two things: was an error level reported, and did execution continue. The meaning of the level, that the behavior may change in the future, does not alter the channel; it only says that this warning may become a `halts` someday. The lesson does not measure that future.

#### Key Types and Merging

Normalization does not work only between strings and integers; Boolean values, the empty value and decimal numbers can also be written in place of a key. To measure which key each one lands on, every candidate is held in a variable and used as the key of a one-element array.

```php
<?php
// types.php — the key that candidates of different types land on
require 'channel.php';

function key_of(mixed $candidate): string
{
    $single = [$candidate => 'x'];
    return var_export(array_key_first($single), true);
}

$candidates = [
    'null'        => null,
    'false'       => false,
    'true'        => true,
    'whole float' => 2.0,
    'empty string' => '',
];

foreach ($candidates as $name => $candidate) {
    [$channel, $evidence] = listen(fn() => key_of($candidate));
    printf("%-14s%-9s%s\n", $name, $channel, $evidence);
}

[$channel, $evidence] = listen(function () {
    $none = null;
    return count([$none => 'a', '' => 'b']);
});
echo 'null and empty string together: ', $channel, ' ', $evidence, "\n";
```

```
null          warning  E_DEPRECATED, result string ''
false         silent   result string 0
true          silent   result string 1
whole float   silent   result string 2
empty string  silent   result string ''
null and empty string together: warning E_DEPRECATED, result int 1
```

A `null` key lands on the empty string and reports `E_DEPRECATED` (**warning**); `false` lands on `0`, `true` on `1`, and `2.0`, which has no fraction, on `2`, and these three, together with the empty string, are **silent**. The last line ties the result together: `null` and `''` are the same key, and an array that writes both carries a single element. Most of these conversions look free of information loss, yet a source that carries `null` and the empty string as separate data loses that distinction once the data enters the array. A decimal number with a fraction, as measured in the previous section, is truncated with a warning.

**AS3.** The five candidates in the table illustrate the language's conversion rule; types that are not valid as keys (another array, an object) are outside this lesson and are not measured here. The lesson reads which type lands on which key from the executed output, not from the written rule.

When two arrays are merged, a key collision is a second point of normalization. Three different merges applied to the same two arrays produce three different fates for the keys.

```php
<?php
// merging.php — merging the same two arrays in three different ways
$a = ['name' => 'Ada', 5 => 'x', 6 => 'y'];
$b = ['name' => 'Can', 5 => 'z'];

echo 'addition: ', json_encode($a + $b), "\n";
echo 'array_merge: ', json_encode(array_merge($a, $b)), "\n";
echo 'array_replace: ', json_encode(array_replace($a, $b)), "\n";
echo 'spread: ', json_encode([...$a, ...$b]), "\n";
echo 'keys: ', implode(',', array_keys(array_merge($a, $b))), "\n";
```

```
addition: {"name":"Ada","5":"x","6":"y"}
array_merge: {"name":"Can","0":"x","1":"y","2":"z"}
array_replace: {"name":"Can","5":"z","6":"y"}
spread: {"name":"Can","0":"x","1":"y","2":"z"}
keys: name,0,1,2
```

The addition operator keeps the values of the left array and adds only the new keys from the right: the `name` key stayed `'Ada'` and the `5` key stayed `'x'`. `array_merge` takes the right value for string keys (`'Can'`) but renumbers integer keys: the keys `5` and `6` disappeared, and all three elements from the two sides were kept under the keys `0, 1, 2`. `array_replace`, by contrast, writes the right value without touching the integer keys. The spread syntax gives the same result as `array_merge`. Four spellings produced three different results for the same two arrays, and none of them reported a level: the channel is **silent**.

**AS4.** The difference between `array_merge` and addition is measured in this lesson only through key collisions. Which is faster, or which should be preferred, is not measured; the lesson does not rank one function above the other, and only shows that the same input gives different results.

#### The Does-the-Key-Exist Question

Whether a key is present in an array can be asked with two separate questions, and the questions do not give the same answer. `isset` asks whether the key exists and its value is not `null`, `array_key_exists` asks only whether the key exists, and `??` supplies the fallback when the value is missing or `null`.

```php
<?php
// exists.php — the difference between isset, array_key_exists and ??
$data = ['full' => 'x', 'empty' => null, 'zero' => 0];

foreach (['full', 'empty', 'zero', 'absent'] as $key) {
    printf(
        "%-6s isset=%-5s exists=%-5s coalesce=%s\n",
        $key,
        var_export(isset($data[$key]), true),
        var_export(array_key_exists($key, $data), true),
        var_export($data[$key] ?? 'fallback', true)
    );
}
```

```
full   isset=true  exists=true  coalesce='x'
empty  isset=false exists=true  coalesce='fallback'
zero   isset=true  exists=true  coalesce=0
absent isset=false exists=false coalesce='fallback'
```

The `empty` line shows the distinction: the key exists, and the value is `null`. `isset` treats it as missing, `array_key_exists` treats it as present, and `??` supplies the fallback. On the `zero` line, all three accept the value as present; `0` is not an empty value. On the `absent` line, where the key truly does not exist, `isset` and `array_key_exists` both give `false`. All three queries returned a `bool` or a fallback value without raising a warning; code that wants to hear about a missing key from a warning must choose the plain read, and the plain read is measured in the next lesson.

#### The Bounding Measurement

Normalization is not applied to every string. To measure which strings are converted to an integer key, string keys can be tried one by one: each is made the key of a one-element array, and `array_key_first` reports the type of that key.

```php
<?php
// bounds.php — which string keys are converted to integers
$candidates = ['0', '1', '-1', '01', '1.5', '-0', ' 1'];

foreach ($candidates as $candidate) {
    $single = [$candidate => 'x'];
    $key = array_key_first($single);
    printf("%-6s%s\n", "'" . $candidate . "'", get_debug_type($key));
}

$two = ['1' => 'a', '01' => 'b'];
echo 'two spellings, element count: ', count($two), "\n";
```

```
'0'   int
'1'   int
'-1'  int
'01'  string
'1.5' string
'-0'  string
' 1'  string
two spellings, element count: 2
```

The rule is read from the output: only strings in **decimal, canonical integer form** are converted to an integer key. `'1'` and `'-1'` become integers; strings with a leading zero (`'01'`), a decimal point (`'1.5'`), a minus-signed zero (`'-0'`) or a space (`' 1'`) stay strings. This distinction draws the limit of the silence in the previous section: if `'1'` and `1` are the same key, `'01'` is a different key, and the two spellings on the last line leave two elements this time. Normalization is not a rule of "merging whatever writes the same number differently"; it is a rule of "converting the canonical spelling to an integer". A data source that writes the same number in two forms produces two separate elements in the array, and that too is reported by no signal.

**AS5.** The types and keys in this section are the result of the language's own rule and rest on a class, not on a single number. The seven candidates measured illustrate the class the language documents and do not claim to be exhaustive. If a string key is not converted to an integer, the reason is that the string is not in canonical decimal form; the lesson does not move on to other bases or to very large integers.

#### Summary

- In PHP, `array` is a single structure: an array whose keys are consecutive integers reads as indexed, and one with string keys reads as associative; order is insertion order.
- `unset` does not renumber keys; an array that looks indexed does not report whether it is still indexed unless it is tested with `array_is_list`.
- `'1'`, `1` and `true` normalize to a single key and one element remains in the array; the measuring channel of this lesson is **silent** (`key normalization`).
- A decimal key such as `1.7` is truncated and `E_DEPRECATED` is reported; a conversion that loses information is heard through the **warning** channel.
- Normalization applies only to strings in decimal, canonical integer form; `'01'`, `'1.5'` and `'-0'` stay strings.

#### Next Step

This lesson measured the keys of a single-level array. When an element of an array is itself another array, the key is no longer a single value but a path, and if one link of the path is missing, `null` arrives. The Multidimensional Arrays lesson measures which channel a missing inner key is heard through, and why writing to the same path is heard through a different channel.

### Multidimensional Arrays

Source: https://academia.sh/en/courses/php-fundamentals/multidimensional-arrays

Nesting arrays inside arrays; a missing inner key giving null with an E_WARNING; array access on null; the null coalescing operator silencing the whole path; reading and writing being separate channels.

The Indexed and Associative Arrays lesson measured how different spellings of the same key land on a single element, and that no channel announces it. There the key was a single value. When an element is itself another array, access becomes a path: `$a[0]['surname']` performs two separate lookups, and the second depends on the result of the first. This lesson measures where it is heard when one link of that path is missing.

A multidimensional array is not a separate type; it is an ordinary array whose elements are arrays. The language sets no limit on depth, and only memory constrains it. The lesson uses the channel names in the sense they had in the previous lessons: **silent**, **return**, **warning**, **halts**.

#### Modeling Nested Data

A list of students, each with a name and a table of grades, fits a nested structure. The file below builds this structure, reads from two levels, and compares the flat count from `count` with the recursive count.

```php
<?php
// students.php — modeling data with arrays inside arrays
$students = [
    ['name' => 'Ada', 'grades' => ['math' => 80, 'science' => 90]],
    ['name' => 'Can', 'grades' => ['math' => 70, 'science' => 60]],
];

foreach ($students as $position => $student) {
    $average = array_sum($student['grades']) / count($student['grades']);
    echo $position, ' ', $student['name'], ' ', $average, "\n";
}

echo 'Can science: ', $students[1]['grades']['science'], "\n";
echo 'flat count: ', count($students), "\n";
echo 'recursive count: ', count($students, COUNT_RECURSIVE), "\n";
```

```
0 Ada 85
1 Can 65
Can science: 60
flat count: 2
recursive count: 10
```

Each level has its own key space. The outer array is indexed, the student record is associative, and the grade table is associative again; `$students[1]['grades']['science']` is the application of three keys in sequence. `count` counts only the outer level, while the `COUNT_RECURSIVE` flag counts every inner array both as an element and through its contents; the difference between the two numbers therefore reflects the depth of the structure and not the number of data items.

**AS6.** In this lesson the records have a fixed shape: every student has a `name` and a `grades` key. The cases where the path breaks are built separately in the next section. Cases where the shape of a record comes from a user or from an external source belong to M16/K01 and are not covered here.

#### A Missing Inner Key

What happens when the path breaks at one link is measured with the listener's channel order. The shared helper of the lesson, `channel.php`, is given below in full; since it has no output block, it is only written to disk.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

```php
<?php
// missing.php — a missing inner key and array access on null
require 'channel.php';

$records = [['name' => 'Ada']];

$observations = [
    'missing inner key'  => fn() => $records[0]['surname'],
    'missing outer key'  => fn() => $records[5]['name'],
    'access on null'     => fn() => $records[5]['name']['x'],
    'access on string'   => fn() => $records[0]['name']['x'],
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-23s%-9s%s\n", $name, $channel, $evidence);
}
```

```
missing inner key      warning  E_WARNING, result null
missing outer key      warning  E_WARNING, result null
access on null         warning  E_WARNING, result null
access on string       halts    TypeError
```

The first line is the course's `missing inner key` observation: `$records[0]['surname']` gave a `null` value and reported `E_WARNING`. The channel is **warning**. Execution continued; because the result is `null`, the calling code learns that something went wrong only from the warning that arrives with the value. This is the distinction between this lesson and the silence of the previous one: a loss in key writing was not heard, and a loss in key reading is heard.

The second line gives the same result for a missing index at the outer level, and the third line shows that, when the first link of the chain is already `null`, the second access reports the same level as well: array access on `null` is a warning and not an error, and `null` is returned again. However long the chain is, every access after a break quietly resolves to `null`; at which link the break occurred is read from the warning text and not from the result. The fourth line shows the boundary: when the path arrives at a string and a string key such as `'x'` is applied to that string, `null` is not returned and a `TypeError` is thrown; the channel is **halts**. The warning channel works only for `null` or a missing key, and not for access applied to a value of the wrong type.

**AS7.** The message text and line information of a warning depend on the environment, and the lesson does not print them; only the level name (`E_WARNING`) and the type of the result are printed. Turning the same break into an error that halts is possible with a setting, but the lesson relies on no ini value and does not measure that route.

#### Silence with Null Coalescing

The null coalescing operator (`??`) gives the value on its left side, or the right side if the left is `null` or undefined. Applied to a missing path, it suppresses the warning at every link of the chain.

```php
<?php
// coalescing.php — ?? turns a missing path into silence
require 'channel.php';

$data = ['a' => 1];
$records = [['name' => 'Ada']];

$observations = [
    'plain read'      => fn() => $data['b'],
    'with coalescing' => fn() => $data['b'] ?? 0,
    'deep coalescing' => fn() => $records[3]['grades']['science'] ?? -1,
    'present value'   => fn() => $records[0]['name'] ?? 'none',
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-19s%-9s%s\n", $name, $channel, $evidence);
}
```

```
plain read         warning  E_WARNING, result null
with coalescing    silent   result int 0
deep coalescing    silent   result int -1
present value      silent   result string Ada
```

The same missing key was heard through two channels: the plain read is a **warning**, and with `??` it is **silent**. The third line matters more: `??` wraps not only the last link but the whole path; in a three-level path, if any link is missing, the result is the default value and no channel reports anything. This is the purpose of the operator, but the purpose also means that the problem disappears: if the path is misspelled (`'grade'` instead of `'grades'`), the `??` spelling covers up the mistake with the same silence.

#### Working with a List of Records

In practice, nested arrays are often a list of records: the outer array is indexed, and each element is an associative array carrying the same keys. Three jobs are common for this shape: extracting a column, sorting the records by a key, and indexing the records by that column.

```php
<?php
// records.php — column, sorting and indexing on a list of records
$records = [
    ['id' => 7, 'name' => 'Can', 'age' => 31],
    ['id' => 3, 'name' => 'Ada', 'age' => 36],
    ['id' => 9, 'name' => 'Efe', 'age' => 24],
];

echo 'names: ', implode(',', array_column($records, 'name')), "\n";
echo 'indexed by id: ', implode(',', array_keys(array_column($records, 'name', 'id'))), "\n";

usort($records, fn($left, $right) => $left['age'] <=> $right['age']);
echo 'by age: ', implode(',', array_column($records, 'name')), "\n";

$records[1]['age'] += 1;
echo 'updated age: ', $records[1]['age'], "\n";
echo 'average age: ', array_sum(array_column($records, 'age')) / count($records), "\n";
```

```
names: Can,Ada,Efe
indexed by id: 7,3,9
by age: Efe,Can,Ada
updated age: 32
average age: 30.666666666667
```

`array_column` turns a column into a flat array; if a third argument is given, the keys of the result come from that column. `usort` rearranges the records according to the given comparison and renumbers the keys. Then `$records[1]['age'] += 1` writes to a two-level path; because the path exists, nothing is reported. Most code that works with a list of records is written with these three functions and two-level indexing; the mistake appears when the records are not all of the same shape.

**AS8.** In this lesson the shape of the records is fixed inside the code. Code that reads the same list from a file or from a request needs to validate the record shape; that validation is not done here, and a function that checks the record shape is outside the scope of this lesson.

When a record is missing a key, extracting the column and reading the key from each record one by one are heard through different channels.

```php
<?php
// missingcolumn.php — array_column silently skips a missing key
require 'channel.php';

$records = [
    ['name' => 'Ada', 'surname' => 'Yilmaz'],
    ['name' => 'Can'],
    ['name' => 'Efe', 'surname' => 'Kaya'],
];

$observations = [
    'array_column'    => fn() => array_column($records, 'surname'),
    'direct read'     => fn() => array_map(fn($record) => $record['surname'], $records),
    'coalescing read' => fn() => array_map(fn($record) => $record['surname'] ?? '-', $records),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-17s%-9s%s\n", $name, $channel, $evidence);
}

echo 'column length: ', count(array_column($records, 'surname')), "\n";
echo 'record count: ', count($records), "\n";
```

```
array_column     silent   result array(2)
direct read      warning  E_WARNING, result array(3)
coalescing read  silent   result array(3)
column length: 2
record count: 3
```

`array_column` silently skipped the record with the missing key: the column carries two elements instead of three, and it belongs to the **silent** channel. The direct read gave `E_WARNING` on the same record and belongs to the **warning** channel; the read with `??` supplied the fallback value and went silent. The three spellings reacted to the same absence in three different ways. The point to watch is this: the result of `array_column` is no longer the same length as the list of records, so code that pairs the two by index silently pairs the wrong record.

#### Destructuring and Recursive Merging

The destructuring syntax, which distributes parts of a nested array into variables, uses its own channel when the path is missing as well. Merging two nested arrays, on the other hand, is done with functions different from flat merging.

```php
<?php
// unpacking.php — destructuring and recursive merging
require 'channel.php';

$observations = [
    'full unpacking'   => fn() => (function () { [$x, $y] = [1, 2]; return $x + $y; })(),
    'missing index'    => fn() => (function () { [$x, $y] = [1]; return $y; })(),
    'missing key'      => fn() => (function () { ['name' => $name, 'age' => $age] = ['name' => 'Ada']; return $age; })(),
    'nested unpacking' => fn() => (function () { [[$p, $q], [$r]] = [[1, 2], [3]]; return $p + $q + $r; })(),
    'missing nested'   => fn() => (function () { [[$p], [$r]] = [[1]]; return $r; })(),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-17s%-9s%s\n", $name, $channel, $evidence);
}

$left = ['a' => ['x' => 1, 'y' => 2], 'b' => 3];
$right = ['a' => ['y' => 20, 'z' => 30], 'b' => 4];
echo 'array_merge: ', json_encode(array_merge($left, $right)), "\n";
echo 'array_replace: ', json_encode(array_replace($left, $right)), "\n";
echo 'array_replace_recursive: ', json_encode(array_replace_recursive($left, $right)), "\n";
echo 'array_merge_recursive: ', json_encode(array_merge_recursive($left, $right)), "\n";
```

```
full unpacking   silent   result int 3
missing index    warning  E_WARNING, result null
missing key      warning  E_WARNING, result null
nested unpacking silent   result int 6
missing nested   warning  E_WARNING, result null
array_merge: {"a":{"y":20,"z":30},"b":4}
array_replace: {"a":{"y":20,"z":30},"b":4}
array_replace_recursive: {"a":{"x":1,"y":20,"z":30},"b":4}
array_merge_recursive: {"a":{"x":1,"y":[2,20],"z":30},"b":[3,4]}
```

Destructuring gives `E_WARNING` on a missing index or a missing key and leaves the variable `null`; the warning channel of reading directly by index therefore applies to destructuring as well. Fully matching and nested destructuring are silent.

In recursive merging, four functions give three distinct results. `array_merge` and `array_replace` replace the inner arrays as a whole, and the `x` key is lost. `array_replace_recursive` descends into the inner arrays and keeps `x`. `array_merge_recursive` turns colliding scalar values into arrays: `[2,20]` for `y` and `[3,4]` for `b`. The silent change in the type of the total value is a detail that can later break code that assumes `b` is a number.

#### The Bounding Measurement

Reading from a missing path gave a warning. Writing to the same missing path is a separate channel.

```php
<?php
// writing.php — writing to a missing path and reading are separate channels
require 'channel.php';

$observations = [
    'deep write to empty' => function () {
        $a = [];
        $a['x']['y'] = 1;
        return $a;
    },
    'read of same path' => function () {
        $a = [];
        return $a['x']['y'];
    },
    'write over a scalar' => function () {
        $a = ['x' => 5];
        $a['x']['y'] = 1;
        return $a;
    },
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-24s%-9s%s\n", $name, $channel, $evidence);
}

$a = [];
$a['x']['y'] = 1;
echo 'built structure: ', json_encode($a), "\n";
```

```
deep write to empty     silent   result array(1)
read of same path       warning  E_WARNING, result null
write over a scalar     halts    Error
built structure: {"x":{"y":1}}
```

Writing `$a['x']['y'] = 1` into an empty array builds the inner array automatically and reports no level: the channel is **silent**. Reading the same path gives a warning. Reading and writing are separate channels; a problem heard in one is not heard in the other. The third line draws the limit of the distinction: if the existing value on the path is a scalar, the write no longer builds automatically and halts.

**AS9.** The silence of writing is designed as a convenience, and on a misspelled key it builds a new branch: `$a['gradez']['math'] = 90` raises no error and opens a new `gradez` branch next to the real one. This lesson does not trace who later reads that branch; all that is measured is that, at the time of writing, nothing is heard through any channel.

#### Summary

- A multidimensional array is not a separate type but an array whose elements are arrays; access is the application of keys in sequence along a path.
- `count` counts only the outer level; `COUNT_RECURSIVE` counts inner arrays both as elements and through their contents.
- A missing inner key returns `null` and reports `E_WARNING`; the measuring channel of this lesson is **warning** (`missing inner key`).
- Array access on `null` also gives `E_WARNING` and returns `null`; every link after the break quietly produces `null`.
- `??` silences the whole path; it covers a misspelled path with the same silence.
- Reading and writing are separate channels: reading a missing path gives a warning, and writing deeply into an empty array builds the inner array silently.

#### Next Step

This lesson measured the break of a path as `null` and a warning. In the Array Functions lesson the channel changes: a search that finds nothing raises no level, and a value is returned instead; and that value can be confused, in a loose comparison, with the return for the first element found.

### Array Functions

Source: https://academia.sh/en/courses/php-fundamentals/array-functions

The filter, map and reduce functions; array_search returning false when it finds nothing; a match at index zero being confused with false in a loose comparison; array_combine with mismatched lengths and a negative str_repeat call throwing ValueError.

In the Multidimensional Arrays lesson, the break of a path was heard as `null` and `E_WARNING`. With array functions the channel changes: when the sought element is not found, no level is reported; the function returns a special value, and interpreting that value becomes the job of the calling code. The lesson uses the channel names in the sense they had in the previous lessons: **silent**, **return**, **warning**, **halts**.

PHP's array functions are a broad family, and the lesson does not walk through all of them. Two groups are measured: the trio of filter, map and reduce, which transforms an array, and the functions that search an array for an element. The first is the data flow itself, and the second is the plainest place to see where a problem is heard from.

#### Filter, Map and Reduce

The three functions ask three separate questions. Filter answers "which elements stay", map answers "what does each element become", and reduce answers "how do they all come down to a single value". The file below runs all three over the same list of numbers.

```php
<?php
// transform.php — filter, map and reduce
$numbers = [1, 2, 3, 4, 5, 6];

$evens = array_filter($numbers, fn($n) => $n % 2 === 0);
$squares = array_map(fn($n) => $n * $n, $numbers);
$total = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);

echo 'evens: ', json_encode($evens), "\n";
echo 'squares: ', json_encode($squares), "\n";
echo 'total: ', $total, "\n";
echo 'keys of evens: ', implode(',', array_keys($evens)), "\n";
echo 'renumbered: ', json_encode(array_values($evens)), "\n";
```

```
evens: {"1":2,"3":4,"5":6}
squares: [1,4,9,16,25,36]
total: 21
keys of evens: 1,3,5
renumbered: [2,4,6]
```

`array_filter` keeps the keys of the retained elements. Because the keys of the `evens` list are `1, 3, 5`, `json_encode` writes it as an object and not as a list; `array_values` renumbers the keys. This is the same behavior as `unset` in the Indexed and Associative Arrays lesson: an array that looks indexed can stop being indexed after filtering. `array_map`, in turn, carries the keys along with the elements; when a single array is given, the result uses the same set of keys.

**AS10.** `array_reduce` binds the first parameter of the callback to an accumulator value, and the initial value is the third argument. If no initial value is given, the accumulator starts as `null`; when an empty array is reduced, the result in that case is `null`. The lesson always gives the initial value explicitly and does not separately measure the empty-array behavior.

#### An Element That Is Not Found

Functions that search for an element report a problem not with a warning but with a return value. `array_search` returns the key of the sought value if it finds it, and `false` if it does not. The listener classifies this behavior with the channel order; `channel.php` is given below in full, and since it has no output block, it is only written to disk.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

```php
<?php
// search.php — an element not found and ValueError
require 'channel.php';

$observations = [
    'element not found' => fn() => array_search('apple', ['pear', 'quince']),
    'element found'     => fn() => array_search('quince', ['pear', 'quince']),
    'first element'     => fn() => array_search('pear', ['pear', 'quince']),
    'negative repeat'   => fn() => str_repeat('a', -1),
    'combine mismatch'  => fn() => array_combine(['a', 'b'], [1]),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-20s%-9s%s\n", $name, $channel, $evidence);
}
```

```
element not found   return   result false
element found       silent   result int 1
first element       silent   result int 0
negative repeat     halts    ValueError
combine mismatch    halts    ValueError
```

The first line is the course's `element not found` observation and belongs to the **return** channel: `array_search('apple', ['pear', 'quince'])` returned `false`, no level was reported, and execution continued. The problem is heard only by code that looks at the return value. The second line shows a successful search: `1`, the key of the element found, is a silent result. The third line prepares the source of the problem: when the first element is found, the key returned is `0`.

The last two lines show another channel of the same family of functions. `str_repeat('a', -1)` and an `array_combine` call with two arrays of different lengths throw `ValueError`; the channel is **halts**. When the value of an argument has the right type but lies outside the acceptable range, the function does not quietly produce a wrong result; it stops. Two different channels exist in the same family of functions: "not found" is a return value, and "invalid call" is an exception.

**AS11.** `array_combine` and `str_repeat` are examples of the `ValueError` channel only. Which function gives `ValueError` or `false` under which condition is written in the documentation of each function, and the lesson does not list it; what is measured is that "not found" comes from the return channel and "invalid call" from the exception channel.

#### Sorting and Slicing

All sorting functions modify the array in place and treat keys differently: `sort` discards the keys and renumbers, `asort` sorts by value while keeping the keys, and `ksort` sorts by key. `array_slice`, in turn, renumbers integer keys by default.

```php
<?php
// sorting.php — the effect of sorting and slicing on keys
$raw = [3 => 'c', 1 => 'a', 2 => 'b'];

$a = $raw;
sort($a);
echo 'sort: ', json_encode($a), "\n";

$b = $raw;
asort($b);
echo 'asort: ', json_encode($b), "\n";

$c = $raw;
ksort($c);
echo 'ksort: ', json_encode($c), "\n";

$d = $raw;
rsort($d);
echo 'rsort: ', json_encode($d), "\n";

echo 'slice: ', json_encode(array_slice($raw, 1)), "\n";
echo 'slice, keys preserved: ', json_encode(array_slice($raw, 1, null, true)), "\n";
echo 'sort return: ', var_export(sort($a), true), "\n";
```

```
sort: ["a","b","c"]
asort: {"1":"a","2":"b","3":"c"}
ksort: {"1":"a","2":"b","3":"c"}
rsort: ["c","b","a"]
slice: ["a","b"]
slice, keys preserved: {"1":"a","2":"b"}
sort return: true
```

`sort` and `rsort` discard the associative information; `asort` and `ksort` keep the key, and `json_encode` therefore writes them as objects. `array_slice` renumbers integer keys unless the `preserve_keys` argument is given. The last line shows a contract the sorting functions keep: they modify the array in place and return `true`; they are not functions that return a sorted copy. Code that writes `$sorted = sort($array)` sorts the array but assigns `true` to the variable `$sorted`.

**AS12.** The stability of the sorting functions (keeping the relative order of equal elements) is not measured in this lesson; the lesson tests only what happens to the keys and the return value. Sorting an array of mixed types depends on the comparison rules of the language, and no example is given here.

#### The Membership Query

`in_array` asks whether a value is present in the array and uses the same loose comparison as `array_search`. When `true` is given as the third argument, the comparison becomes strict.

```php
<?php
// membership.php — in_array with loose and strict comparison
$numbers = ['10', '20'];

echo "loose, '1e1': ", var_export(in_array('1e1', $numbers), true), "\n";
echo "strict, '1e1': ", var_export(in_array('1e1', $numbers, true), true), "\n";
echo "loose, 10: ", var_export(in_array(10, $numbers), true), "\n";
echo "strict, 10: ", var_export(in_array(10, $numbers, true), true), "\n";
echo "loose null, [0]: ", var_export(in_array(null, [0]), true), "\n";
echo "strict null, [0]: ", var_export(in_array(null, [0], true), true), "\n";

$repeated = ['1', 1, 'a', 'A', 'a'];
echo 'unique: ', json_encode(array_unique($repeated)), "\n";
```

```
loose, '1e1': true
strict, '1e1': false
loose, 10: true
strict, 10: false
loose null, [0]: true
strict null, [0]: false
unique: {"0":"1","2":"a","3":"A"}
```

In a loose comparison, the string `'1e1'` counts as equal to `'10'`, because both are numeric strings and their value is `10`; the strict comparison separates the two. The same rule holds for the integer `10` and the string `'10'`. `null` and `0` are equal in a loose comparison and not in a strict one. These results are not a badly written condition; they are the rule of loose equality, and the coercion measured in the Programming Fundamentals course (M06/K01) affects the result of a search here. Because the output of `in_array` is only `true` or `false`, a false positive is heard through no channel: the channel is **silent**. On the last line, `array_unique` keeps the first value it sees and counts `'1'` and `1`, whose string representation is the same, as one; `'a'` and `'A'` stay separate.

#### Silent Behaviors in Mapping and Summing

Mapping, flipping and summing functions often report no level when the shape of the input differs from what was expected. The observations below are classified with the same listener.

```php
<?php
// quiet.php — input problems in mapping, flipping and summing
require 'channel.php';

$observations = [
    'two arrays, unequal'  => fn() => array_map(fn($a, $b) => [$a, $b], [1, 2, 3], ['x']),
    'duplicate value flip' => fn() => array_flip(['a', 'b', 'a']),
    'duplicate key'        => fn() => array_combine(['a', 'a'], [1, 2]),
    'numeric string sum'   => fn() => array_sum(['3', '4']),
    'non-numeric sum'      => fn() => array_sum([1, 'abc']),
    'empty array sum'      => fn() => array_sum([]),
    'empty array max'      => fn() => max([]),
    'key filter'           => fn() => array_filter(['a' => 1, 'b' => 2], fn($k) => $k === 'a', ARRAY_FILTER_USE_KEY),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-23s%-9s%s\n", $name, $channel, $evidence);
}

echo 'flip: ', json_encode(array_flip(['a', 'b', 'a'])), "\n";
echo 'combine: ', json_encode(array_combine(['a', 'a'], [1, 2])), "\n";
echo 'map: ', json_encode(array_map(fn($a, $b) => [$a, $b], [1, 2, 3], ['x'])), "\n";
```

```
two arrays, unequal    silent   result array(3)
duplicate value flip   silent   result array(2)
duplicate key          silent   result array(1)
numeric string sum     silent   result int 7
non-numeric sum        warning  E_WARNING, result int 1
empty array sum        silent   result int 0
empty array max        halts    ValueError
key filter             silent   result array(1)
flip: {"a":2,"b":1}
combine: {"a":2}
map: [[1,"x"],[2,null],[3,null]]
```

When `array_map` receives two arrays, it pads the shorter one with `null` and reports no level; the pairs `[2,null]` and `[3,null]` are produced silently. `array_flip` and `array_combine` keep the last value on a duplicate value or key and silently discard the first: `{"a":2,"b":1}` and `{"a":2}`.

In summing, numeric strings are converted to numbers (the sum of `'3'` and `'4'` is 7), whereas a non-numeric element produces `E_WARNING` and the total counts only the valid elements. On an empty array, `array_sum` gives 0 and `max` halts with `ValueError`; on an array that may be empty, `max` must be guarded before the call with `count` or `[] !== $array`. `ARRAY_FILTER_USE_KEY` gives the key to the callback instead of the value and works silently.

#### The Bounding Measurement

The return channel works only if the calling code reads it correctly. The returns `0` and `false` of `array_search` are the same value in a loose equality. Two spellings interpret the same list in two different ways.

```php
<?php
// distinction.php — 0 and false are the same value in a loose comparison
$items = ['pear', 'quince'];

$loose = array_search('pear', $items) == false ? 'not found' : 'found';
$strict = array_search('pear', $items) === false ? 'not found' : 'found';

echo 'loose: ', $loose, "\n";
echo 'strict: ', $strict, "\n";
echo 'really absent, loose: ',
    (array_search('apple', $items) == false ? 'not found' : 'found'), "\n";

$mixed = array_filter([1, 0, 2, 0, 3]);
echo 'zeros dropped: ', json_encode($mixed), "\n";
echo 'keys: ', implode(',', array_keys($mixed)), "\n";
```

```
loose: not found
strict: found
really absent, loose: not found
zeros dropped: {"0":1,"2":2,"4":3}
keys: 0,2,4
```

The loose comparison that searched for the first element gave the result `not found`, and the strict comparison gave `found`; on an element that is truly absent, the loose comparison gave the correct result. The same call answers wrongly when the sought element is first in the list and correctly when it is elsewhere, and no channel reports this difference. The discriminating power of the return channel is limited by the discriminating power of the caller: if `0` and `false` are not told apart, the first element found counts as "absent".

When `array_filter` is called without a callback, every element whose value is equal to `false` is dropped; `0` is one of them. The keys in the output stay `0, 2, 4`: data is lost and the array stops being indexed, and both are silent. If filtering is what was intended, there is no problem; if the aim was only to drop empty values, `0` is dropped too, though it was an expected value.

#### Summary

- `array_filter` keeps keys, `array_map` carries keys along, and `array_reduce` comes down to a single value with an initial value.
- After filtering, an array can stop being indexed; `array_values` renumbers the keys.
- `array_search` returns `false` when it finds nothing; the measuring channel of this lesson is **return** (`element not found`).
- An invalid argument value (`str_repeat('a', -1)`, `array_combine` with mismatched lengths) throws `ValueError`; the channel is **halts**.
- The `0` returned when the first element is found is the same as `false` in a loose comparison; only a strict comparison separates the two.
- `array_filter` without a callback silently drops every element equal to `false`, `0` included.

#### Next Step

This lesson measured the discriminating power of the return channel and showed that, once `0` and `false` are mixed up, the distinction is left to the caller. The String Handling lesson measures a harsher case of the same channel: if two separate problems give the same return value, the value alone cannot say which of them it is.

### String Handling

Source: https://academia.sh/en/courses/php-fundamentals/string-handling

Formatting with sprintf and printf; the byte and character distinction between strlen and mb_strlen; malformed JSON coming back from json_decode as null; telling the cases apart with json_last_error and JSON_THROW_ON_ERROR; the behavior of implode and strlen on an array argument.

The Array Functions lesson showed that the discriminating power of the return channel is limited by the discriminating power of the caller: once `0` and `false` were not told apart, the first element found counted as "absent". String handling brings out the same limit in a harsher form. The `null` returned when a string is decoded can come from a valid `null` value or from malformed input, and the value itself does not say which one it is.

A string in PHP is a sequence of bytes; how many bytes a character takes depends on the function that reads the string. The lesson covers two subjects: the functions that format and measure a string, and the functions that encode and decode a string as JSON. The channel names are used in the sense they had in the previous lessons: **silent**, **return**, **warning**, **halts**.

#### Formatting and Byte Measurement

`sprintf` places values into a format string; `printf` does the same formatting and prints it directly. Width, alignment and the number of decimal places are given with a specifier.

```php
<?php
// format.php — formatting with sprintf
echo sprintf('%05d', 42), "\n";
echo sprintf('%.2f', 3.14159), "\n";
echo sprintf('%-8s|', 'php'), "\n";
echo sprintf('%8s|', 'php'), "\n";
echo sprintf('%s book, %d pages', 'Lesson', 120), "\n";
printf("%x %o %b\n", 255, 8, 5);
echo sprintf('%1$s-%2$s-%1$s', 'a', 'b'), "\n";
```

```
00042
3.14
php     |
     php|
Lesson book, 120 pages
ff 10 101
a-b-a
```

The width specifier counts bytes, not characters. The difference shows in a string that contains a multibyte letter; the file below prints only lengths and does not build an aligned column.

```php
<?php
// bytes.php — strlen counts bytes, mb_strlen counts characters
$name = 'señor';

echo 'strlen: ', strlen($name), "\n";
echo 'mb_strlen: ', mb_strlen($name), "\n";

$padded = sprintf('%-8s', $name);
echo 'padded bytes: ', strlen($padded), "\n";
echo 'padded characters: ', mb_strlen($padded), "\n";

echo 'ascii strlen: ', strlen('senor'), "\n";
echo 'ascii mb_strlen: ', mb_strlen('senor'), "\n";
```

```
strlen: 6
mb_strlen: 5
padded bytes: 8
padded characters: 7
ascii strlen: 5
ascii mb_strlen: 5
```

`strlen('señor')` gave six and `mb_strlen('señor')` gave five: `ñ` is two bytes and a single character. The string padded to eight bytes with `sprintf('%-8s')` looks only seven characters wide; in an aligned column, this shift pulls the next column to the left. For the all-ASCII `senor`, the two functions give the same number, so the difference appears only when multibyte data arrives. The problem is reported through no channel: **silent**. Before data that contains multibyte letters is placed in an aligned column, the width must be calculated over characters, not bytes.

**AS13.** This lesson uses only `mb_strlen` from the `mb_` family and assumes the input is in a single encoding (UTF-8). Input whose encoding is unspecified or mixed lies outside this lesson and is not measured.

#### Malformed JSON

JSON is an interchange format that turns arrays and associative arrays into text. `json_encode` converts a PHP value to text, and `json_decode` converts text to a PHP value. The listener classifies what each does when it fails; `channel.php` is given below in full, and since it has no output block, it is only written to disk.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

```php
<?php
// json.php — problem channels in JSON and string operations
require 'channel.php';

$observations = [
    'malformed json'     => fn() => json_decode('{broken'),
    'valid json'         => fn() => json_decode('{"a":1}', true),
    'bad encoding'       => fn() => json_encode("\xB1"),
    'encoding exception' => fn() => json_encode("\xB1", JSON_THROW_ON_ERROR),
    'nested implode'     => fn() => implode(',', [1, [2, 3]]),
    'strlen on array'    => fn() => strlen([1, 2]),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-19s%-9s%s\n", $name, $channel, $evidence);
}
```

```
malformed json     return   result null
valid json         silent   result array(1)
bad encoding       return   result false
encoding exception halts    JsonException
nested implode     warning  E_WARNING, result string 1,Array
strlen on array    halts    TypeError
```

The first line is the course's `malformed json` observation: `json_decode('{broken')` returned `null`, and no level was reported. The class is **return**. The third line is the encoding direction of the same channel: encoding an invalid byte sequence returns `false`. Both say that an error occurred only through the return value.

The fourth line shows the channel changing. When the `JSON_THROW_ON_ERROR` flag is given, the same problem is thrown as `JsonException`; the class is **halts**. The flag moves a problem on the return channel to the exception channel and hands control to the caller: if the caller does not catch it, execution stops. The fifth line is the third channel of string handling: when a nested array is passed to `implode`, the inner array is converted to the string `'Array'` and `E_WARNING` is reported, that is, **warning**. The last is the type channel: when `strlen` is called with an array argument, it throws `TypeError` and halts.

#### Text Search and Cutting

`strpos` returns `0` when the sought text is at the start and `false` when it is not found at all. This is the same channel and the same trap as `array_search` in the Array Functions lesson: a loose comparison does not tell the two apart. PHP offers `str_contains` for this job, which does not require looking at a return value.

```php
<?php
// searching.php — string search and cutting functions
$text = 'php lesson';

echo 'strpos at start: ', var_export(strpos($text, 'php'), true), "\n";
echo 'strpos missing: ', var_export(strpos($text, 'js'), true), "\n";
echo 'loose at start: ', (strpos($text, 'php') == false ? 'not found' : 'found'), "\n";
echo 'str_contains at start: ', var_export(str_contains($text, 'php'), true), "\n";
echo 'substr: ', var_export(substr($text, 4), true), "\n";
echo 'substr beyond: ', var_export(substr($text, 20), true), "\n";
echo 'explode: ', json_encode(explode(' ', $text)), "\n";
echo 'empty separator: ', json_encode(explode(',', '')), "\n";
echo 'trim: ', var_export(trim("  a b \n"), true), "\n";
echo 'str_replace: ', str_replace('php', 'go', $text), "\n";
```

```
strpos at start: 0
strpos missing: false
loose at start: not found
str_contains at start: true
substr: 'lesson'
substr beyond: ''
explode: ["php","lesson"]
empty separator: [""]
trim: 'a b'
str_replace: go lesson
```

`strpos` returned `0` for the sought text at the start, and the loose comparison counted it as `not found`; `str_contains` answered the same question correctly with `true`. `substr` returns an empty string if the start is beyond the end of the string and gives no error. When `explode` splits an empty string, it gives not an empty list but a list with a single empty string: the one-element `[""]`. This result is a boundary condition, and it silently erases the distinction between "no data" and "empty data present".

**AS14.** `str_contains`, `str_starts_with` and their relatives do not resolve the problem as a channel; they only turn the question into the right type. Writing a usage guide is not the job of this lesson, and no claim is made about behaviors that do not appear in the table above.

A function that cuts a multibyte letter at the byte level produces an invalid string, and that string causes a problem at the next encoding call. Converting to uppercase depends on the same byte and character distinction.

```php
<?php
// cut.php — the effect of byte-level cutting and uppercasing
$name = 'señor';

$byteCut = substr($name, 0, 3);
$characterCut = mb_substr($name, 0, 3);

echo 'byte cut length: ', strlen($byteCut), "\n";
echo 'byte cut valid: ', var_export(mb_check_encoding($byteCut, 'UTF-8'), true), "\n";
echo 'byte cut json: ', var_export(json_encode($byteCut), true), "\n";
echo 'character cut length: ', mb_strlen($characterCut), "\n";
echo 'character cut valid: ', var_export(mb_check_encoding($characterCut, 'UTF-8'), true), "\n";
echo 'uppercase same: ', var_export(strtoupper($name) === mb_strtoupper($name), true), "\n";
```

```
byte cut length: 3
byte cut valid: false
byte cut json: false
character cut length: 3
character cut valid: true
uppercase same: false
```

`substr($name, 0, 3)` took three bytes and cut off the first byte of the letter `ñ`; the result is not a valid UTF-8 string, and `json_encode` rejected it by returning `false`. `mb_substr` made the same cut on a character boundary, and the result stayed valid. `strtoupper` uppercases only ASCII letters; `mb_strtoupper` converts the multibyte letter as well, so the two results did not come out equal. The choice between bytes and characters in a cutting or uppercasing function announces the problem through no channel: the cut string is noticed only afterwards, at the encoding stage, through a return value.

#### The Limits of Format Specifiers

`sprintf` matches the specifiers in the format string with the arguments; the number, type and syntax of the specifiers determine the channel of the call.

```php
<?php
// specifiers.php — the limits of sprintf specifiers
require 'channel.php';

$observations = [
    'width and precision' => fn() => sprintf('%05.1f|%-4s|', 3.14159, 'ab'),
    'pad character'       => fn() => sprintf("%'*8s", 'php'),
    'signed number'       => fn() => sprintf('%+d', 5),
    'string as number'    => fn() => sprintf('%d', 'abc'),
    'missing argument'    => fn() => sprintf('%s %s', 'a'),
    'unknown specifier'   => fn() => sprintf('%y', 1),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-21s%-9s%s\n", $name, $channel, $evidence);
}
```

```
width and precision  silent   result string 003.1|ab  |
pad character        silent   result string *****php
signed number        silent   result string +5
string as number     silent   result string 0
missing argument     halts    ArgumentCountError
unknown specifier    halts    ValueError
```

The first three lines show the specifiers working normally and are silent. The fourth line is the real point: the `%d` specifier converts a non-numeric string to `0` and reports no level; data of the wrong type is lost silently in formatting. When there are fewer arguments than specifiers, `ArgumentCountError` is thrown, and when an undefined specifier is written, `ValueError` is thrown; both **halt**. The same function is silent for one kind of error and halting for another: the channel depends on the kind of error in the call.

#### Padding, Number Format and Comparison

A few helper functions used to display numbers and strings move details such as alignment and separators into their arguments.

```php
<?php
// helpers.php — padding, number format and string comparison
require 'channel.php';

echo number_format(1234567.891, 2, ',', '.'), "\n";
echo number_format(0.5), ' ', number_format(1.5), ' ', number_format(2.5), "\n";
echo str_pad('7', 3, '0', STR_PAD_LEFT), "|", str_pad('ab', 6, '-', STR_PAD_BOTH), "|\n";
echo ucwords('php string handling'), "\n";
echo strrev('abc'), ' ', str_word_count('one two three'), "\n";
echo 'strcmp: ', strcmp('a', 'b'), ' ', strcmp('b', 'a'), ' ', strcmp('a', 'a'), "\n";
echo 'spaceship: ', 'a' <=> 'b', ' ', 'b' <=> 'a', ' ', 'a' <=> 'a', "\n";

$observations = [
    'numeric loose equal'  => fn() => '10' == '1e1',
    'numeric strict equal' => fn() => '10' === '1e1',
    'leading numeric'      => fn() => '12abc' + 1,
    'wholly non-numeric'   => fn() => 'abc' + 1,
    'integer cast'         => fn() => (int) '12abc',
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-22s%-9s%s\n", $name, $channel, $evidence);
}
```

```
1.234.567,89
1 2 3
007|--ab--|
Php String Handling
cba 3
strcmp: -1 1 0
spaceship: -1 1 0
numeric loose equal   silent   result true
numeric strict equal  return   result false
leading numeric       warning  E_WARNING, result int 13
wholly non-numeric    halts    TypeError
integer cast          silent   result int 12
```

`number_format` rounds at the decimal (`1 2 3` for `0.5`, `1.5`, `2.5`), and the thousands and decimal separators are set with arguments. `str_pad` pads on the left, on the right or on both sides. `strcmp` and `<=>` give the same three values for sorting.

The main point to watch in comparison is numeric strings: `'10' == '1e1'` is true because both sides are converted to numbers, whereas `===` gives false. `'12abc' + 1` produces 13 but gives `E_WARNING`, and `'abc' + 1` stops with `TypeError`. `(int) '12abc'` silently gives 12; for the same input, conversion is silent, arithmetic gives a warning, and arithmetic on a wholly non-numeric string halts.

#### The Bounding Measurement

The return channel cannot tell cases apart. Decoding a valid `null` value and decoding malformed input give the same value.

```php
<?php
// distinguish.php — a null return can come from two separate cases
$broken = json_decode('{broken');
echo 'broken: ', var_export($broken, true), ' error ', json_last_error(), "\n";

$valid = json_decode('null');
echo 'valid null: ', var_export($valid, true), ' error ', json_last_error(), "\n";

echo 'equal: ', var_export($broken === $valid, true), "\n";
echo 'error state: ', json_last_error() === JSON_ERROR_NONE ? 'none' : 'present', "\n";

try {
    json_decode('{broken', false, 512, JSON_THROW_ON_ERROR);
    echo "broken: did not throw\n";
} catch (JsonException $e) {
    echo 'broken: ', get_class($e), ' code ', $e->getCode(), "\n";
}

$result = json_decode('null', false, 512, JSON_THROW_ON_ERROR);
echo 'valid null, with flag: ', var_export($result, true), "\n";
```

```
broken: NULL error 4
valid null: NULL error 0
equal: true
error state: none
broken: JsonException code 4
valid null, with flag: NULL
```

Both calls returned `null` and came out equal under `===`. The distinction is made by `json_last_error()`: four (`JSON_ERROR_SYNTAX`) for malformed input, zero for a valid `null`. This function carries the error state of the last call; the next `json_decode` call resets it, so it must be read immediately afterwards. The more robust route is the `JSON_THROW_ON_ERROR` flag: malformed input throws `JsonException`, a valid `null` comes back as `null`, and the two cases now arrive through separate channels.

**AS15.** `json_last_error()` holds a global error state, and the lesson reads it only right after a `json_decode` call. Cases where another function intervenes are not measured. The lesson relies on no ini value; the `JSON_THROW_ON_ERROR` flag is passed to the call explicitly.

#### Summary

- `sprintf` and `printf` place values according to a format string; the width specifier counts bytes, not characters.
- `strlen('señor')` gives six and `mb_strlen('señor')` gives five; before data that contains multibyte letters is placed in an aligned column, the width must be calculated in characters.
- `json_decode('{broken')` returns `null`; the measuring class of this lesson is **return** (`malformed json`). For an invalid byte sequence, `json_encode` returns `false`.
- `JSON_THROW_ON_ERROR` throws the same problem as `JsonException`; the class is **halts**.
- A valid `null` and malformed JSON both return `null`; the distinction is made by `json_last_error()` (zero or four) or by `JSON_THROW_ON_ERROR`.
- `implode` on a nested array gives `'Array'` and `E_WARNING`; `strlen` with an array argument throws `TypeError`.

#### Next Step

Problems in arrays and strings can be heard through four channels: silent, return, warning and halts. The next topic is the place where data comes not from inside the code but from outside. The Superglobals lesson measures which channel is heard when request data is missing.

## Request and Response

Request data arriving through superglobals, form data received and validated, the limits of an uploaded file, and the difference between the two forms of including a file.

### Superglobals

Source: https://academia.sh/en/courses/php-fundamentals/superglobals

How request data reaches PHP through superglobals: a missing key heard through the warning channel, values that are always strings, and the client choosing the type of a value.

The String Handling lesson measured that `json_decode` returns `null` once malformed data is decoded, and that the return channel cannot tell this `null` apart from a valid `null` value. Telling them apart was left to the caller, who had to ask an extra question. Up to that lesson all of the data lived inside the program: array literals, string literals, one JSON text. In this lesson the data arrives from outside, and the question stays the same: what is heard when request data arrives missing or in an unexpected form?

M04 established the structure of an HTTP request and M16/K01 the server-side life cycle of a request; here only the mechanism PHP uses to hand over this data is measured. The mechanism is called a **superglobal**: a predefined array that PHP fills at the start of the request and that is reachable from every scope without being declared. Four of them matter for request data: `$_GET` carries the query string, `$_POST` the form fields in the request body, `$_SERVER` request information such as the method, and `$_FILES` the uploaded files. The last one is the subject of the third lesson.

#### A Missing Key Raises a Warning

The first measurement runs on the command line. There is no request there and `$_GET` is empty; reading a key that does not exist behaves the same as asking for a variable that is absent from a query string. The measurement uses `channel.php`, the core of the course, which is given verbatim in this lesson as well.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

```php
<?php
// missing.php — which channel is heard when reading from an empty $_GET
require 'channel.php';

$measurements = [
    'missing query variable' => fn() => $_GET['page'],
    'null coalescing'        => fn() => $_GET['page'] ?? 1,
    'filter_input'           => fn() => filter_input(INPUT_GET, 'page'),
];

foreach ($measurements as $name => $measurement) {
    [$channel, $evidence] = listen($measurement);
    printf("%-24s%-10s%s\n", $name, $channel, $evidence);
}
```

```
missing query variable  warning   E_WARNING, result null
null coalescing         silent    result int 1
filter_input            return    result null
```

The first row is the core observation `missing query variable`: the result is `null`, but an `E_WARNING` was reported and execution continued. This is the **warning** channel. The caller did not have to ask anything; the channel spoke on its own. The read happened inside a function, without a `global` declaration, which is exactly what defines a superglobal. **RR1.** `$_GET` is empty in this measurement because execution is on the command line; the channel of the row follows from the key's absence in the array and does not depend on the source of the request.

The second row is the opposite channel. The null coalescing operator removes the warning, and the same missing key is heard through the **silent** channel; it is the request-data counterpart of the measurement in the Operators lesson. Meeting a missing key with a default is a deliberate choice, and this is the right tool for it, but no channel reports the absence. The third row is `filter_input`: on the command line there is no request, so it returns `null` and no signal is raised; the problem is heard only by a caller that looks at the return value. This is the **return** channel, and the functions of the next lesson use the same channel.

#### Data Arriving at the Server

For a real request, the **built-in server** that ships with PHP is used: the `php -S` command starts a server at the given address and executes the requested script from the beginning for each request. The measurement has two parts: a script that prints the request data, and a shell script that starts the server, sends the requests and then shuts it down. **RR2.** Every `php -S` call is started with `display_errors` off; with the setting off, an uncaught error produces status code 500, and no error text falls into the lesson output.

```php
<?php
// request.php — prints the type of the incoming data and the request method
foreach ($_GET as $name => $value) {
    $length = is_array($value) ? count($value) : strlen($value);
    echo 'get ', $name, '=', get_debug_type($value), ' ', $length, "\n";
}
foreach ($_POST as $name => $value) {
    echo 'post ', $name, '=', get_debug_type($value), ' ', strlen($value), "\n";
}
echo 'method=', $_SERVER['REQUEST_METHOD'], "\n";
```

```bash
# server.sh — starts the built-in server, sends the requests, shuts it down
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/request.php && break; sleep 0.1; done
echo '--- GET ?n=5&b='
curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/request.php?n=5&b='
echo '--- GET ?a[]=1&a[]=2'
curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/request.php?a[]=1&a[]=2'
echo '--- POST body x=1, query n=5'
curl -sg -d 'x=1' -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/request.php?n=5'
kill $pid
```

```
--- GET ?n=5&b=
get n=string 1
get b=string 0
method=GET
status=200
--- GET ?a[]=1&a[]=2
get a=array 2
method=GET
status=200
--- POST body x=1, query n=5
get n=string 1
post x=string 1
method=POST
status=200
```

Three results hold throughout this lesson. First, every value is a **string**: the `5` that arrives with `?n=5` is not an `int` but a `string` of length 1, and the empty value that arrives with `?b=` is not `null` or `false` but a `string` of length 0. The request travels as text and PHP makes no type decision; when a number is expected, converting the string is the code's job. Second, appending square brackets to the name (`a[]`) turns the value into an **array**: when the same name arrives twice it becomes an `array`, and the element count shows 2. Third, in a POST request the query string still lands in `$_GET`, the field in the body goes to `$_POST`, and `REQUEST_METHOD` reports the method as `POST`; so that the two sources do not get mixed up, the code states explicitly which one it reads from.

**RR3.** What gets printed is not the values themselves but their type and length, which keeps the output independent of the environment. The status code is 200 because `request.php` raised no error.

#### Headers and the Raw Query String Live in `$_SERVER`

The parsed form of the query string is in `$_GET`; the raw form and the request headers are in the `$_SERVER` array. The `QUERY_STRING` key carries the unparsed text, and keys that begin with the `HTTP_` prefix carry the headers, uppercased and with hyphens converted to underscores. The client chooses the header, so it may be absent, and reading a missing header uses the same channel as a missing query variable.

```php
<?php
// header.php — the raw query string and a custom request header
echo 'method=', $_SERVER['REQUEST_METHOD'], "\n";
echo 'query=', $_SERVER['QUERY_STRING'], "\n";
echo 'header=', $_SERVER['HTTP_X_CLIENT'] ?? 'none', "\n";
echo 'parsed=', count($_GET), "\n";
```

```bash
# header.sh — sends two requests, one with the header and one without
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/header.php && break; sleep 0.1; done
echo '--- with header'
curl -s -H 'X-Client: test' 'http://127.0.0.1:8081/header.php?a=1&b=2&a=3'
echo '--- without header'
curl -s 'http://127.0.0.1:8081/header.php'
kill $pid
```

```
--- with header
method=GET
query=a=1&b=2&a=3
header=test
parsed=2
--- without header
method=GET
query=
header=none
parsed=0
```

In the raw string `a` appears twice, but `$_GET` counts only two keys: a repeated name collapses to a single key holding the last value. The header appears under the `HTTP_X_CLIENT` key when the client sends it; when it does not, the key is absent and the null coalescing operator supplies the default. **RR4.** The header value is in the client's hands, so it is not a trustworthy source; what this lesson measures is through which channel the arrival or absence of the header is heard, and the null coalescing operator used here moves it to the silent channel.

#### An Empty Value Gets Three Results from Three Tests

Because request data arrives as strings, the question "is there a value" can be asked with three different operations: `isset`, `empty` and the null coalescing operator. None of the three raises a warning for a missing key, so all three belong to the silent channel. The difference shows up in what the problem is: the `0` that arrives with `?n=0` and the empty string that arrives with `?b=` are two cases in which the key exists but the value looks empty.

```php
<?php
// blank.php — the result of three tests on values that exist but look empty
foreach (['n', 'b', 'none'] as $name) {
    $present = isset($_GET[$name]) ? 'yes' : 'no';
    $blank = empty($_GET[$name]) ? 'yes' : 'no';
    $fallback = $_GET[$name] ?? 'default';
    printf("%-5s isset=%-6s empty=%-6s coalesce=[%s]\n", $name, $present, $blank, $fallback);
}
```

```bash
# blank.sh — calls blank.php with a zero, an empty string and a missing key
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/blank.php && break; sleep 0.1; done
curl -s -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/blank.php?n=0&b='
kill $pid
```

```
n     isset=yes    empty=yes    coalesce=[0]
b     isset=yes    empty=yes    coalesce=[]
none  isset=no     empty=yes    coalesce=[default]
status=200
```

`isset` says `yes` for both existing keys and `no` for the missing one. `empty` treats both the string `0` and the empty string as empty; even though the value of `n` is a valid number, `empty` classifies it as "no value". The null coalescing operator keeps the empty string and falls to the default only for a missing key. **RR5.** The three tests make three different decisions on the same input and none of them produces a signal: code that picks the wrong test silently counts a valid `0` as missing. This is the request-data counterpart of the not operator problem on the return channel (measured in the Form Handling lesson).

#### The Merged Array Depends on an ini Value

The `$_REQUEST` array collects the contents of `$_GET` and `$_POST`, and depending on the setting `$_COOKIE` as well, in a single array. When the same name exists in both sources, the `request_order` ini value decides which one wins. The lesson trusts no ini value, so the setting is passed with `-d` and both orders are measured.

```php
<?php
// merged.php — what $_REQUEST gives when the same name arrives in both the query string and the body
echo 'get=', $_GET['x'], ' post=', $_POST['x'], ' request=', $_REQUEST['x'], "\n";
```

```bash
# merged.sh — tries two different orders of request_order
for order in GP PG; do
  php -S 127.0.0.1:8081 -d display_errors=0 -d request_order=$order >/dev/null 2>&1 &
  pid=$!
  for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/merged.php && break; sleep 0.1; done
  echo "--- request_order=$order"
  curl -s -d 'x=body' 'http://127.0.0.1:8081/merged.php?x=query'
  kill $pid
  wait $pid 2>/dev/null
done
true
```

```
--- request_order=GP
get=query post=body request=body
--- request_order=PG
get=query post=body request=query
```

`$_GET` and `$_POST` give the same values under both orders, but `$_REQUEST` changes: with the order `GP` the value in the body wins, with `PG` the value in the query string wins. **RR6.** The same request reads different data depending on the setting's value, and no channel announces it. So if the code cares about which source the data came from, it reads explicitly from either `$_GET` or `$_POST`.

#### The Bounding Measurement

The second finding in the Data Arriving at the Server section raises a problem: the client can send an array instead of a string for any name. If the script expects a string and the client sends an array with `?u[]=x`, `strlen` throws a `TypeError` on that array argument. This is an uncaught error and belongs to the **halts** channel. The author of the code assumed a missing key would be heard through the warning channel, yet the client chose the channel. The channel of a piece of code is never bound to the code alone, because the client determines the input.

How the same error appears depends on whether it arrives before or after the output. Two scripts make the same `strlen` call; one makes the call before any output, the other after an `echo`.

```php
<?php
// early.php — the error arrives before any output starts
$u = strlen($_GET['u']);
echo "length=$u\n";
```

```php
<?php
// late.php — the same error arrives after an echo
echo "started\n";
$u = strlen($_GET['u']);
echo "length=$u\n";
```

```bash
# type.sh — the status code the same array input gives in two scripts
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/early.php && break; sleep 0.1; done
echo '--- early.php string'
curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/early.php?u=abc'
echo '--- early.php array'
curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/early.php?u[]=x'
echo '--- late.php array'
curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/late.php?u[]=x'
kill $pid
```

```
--- early.php string
length=3
status=200
--- early.php array
status=500
--- late.php array
started
status=200
```

When a string is sent, everything works. When an array is sent, `early.php` prints nothing and returns **500**; `late.php` has already printed the `started` line and sent the headers, so it returns **200** with a partial body. Same request, same error class, two different status codes. **RR7.** If the error arrives before the response body starts, the server has not yet sent the headers and can produce a 500; once the headers have gone, the status code cannot change. The status codes established in M04 report the result of a request, but here what the code tells is where in the execution the error arose, not whether there was an error. Hence **the status code is not a reliable measure of the channel**: a client that sees 200 cannot know that an uncaught error occurred on the server.

#### Summary

- Superglobals (`$_GET`, `$_POST`, `$_SERVER`, `$_FILES`) are reachable from every scope without being declared, and they are the mechanism through which PHP hands over request data.
- The channel measured in this lesson is **warning**: reading a key that does not exist from an empty `$_GET` gives `null` and reports an `E_WARNING`; execution continues.
- The null coalescing operator moves the same absence to the silent channel; `filter_input` on the command line returns `null` and so announces it through the return channel.
- Request data arrives as `string`, a name in the form `a[]` makes it an `array`; in a POST request the query string lands in `$_GET` and the body in `$_POST`.
- Because the client can choose the type, the channel is in the client's hands as well: sending an array makes `strlen` throw a `TypeError`, and the status code is 500 or 200 depending on whether the error arrives before or after the output.

#### Next Step

In this lesson the key was either missing or arrived with the type the client chose, but the content of the value was never questioned. Who decides whether a value that arrives as a string is a number? The Form Handling lesson measures how an invalid value such as `12a` is filtered with `filter_var` and with which return value the function reports the failure; whether a valid `0` gets confused with that same value is also that lesson's question.

### Form Handling

Source: https://academia.sh/en/courses/php-fundamentals/form-handling

Validating strings that arrive from a form with filter_var: failure heard through the return value, converted to null, and a valid 0 confused with failure.

In the Superglobals lesson, request data always arrived as a string and a missing key was heard through the warning channel. In this lesson the key exists, but its content is not in the expected form: a field that should hold a number received `12a`. Does PHP say anything when it meets this value, and if it does, through which channel?

M14 covered the HTML structure of a form and how its fields are built; here only the server-side filtering of the submitted data is measured. PHP's `filter_var` function does the validation work: it takes a value and a filter constant, and if the filter accepts the value it returns the value in the appropriate type, otherwise it returns `false`. `htmlspecialchars` handles escaping in output; why escaping is needed and how the defenses against the underlying problem are classified is the subject of M25.

#### An Invalid Value Is Heard Through the Return

The first measurement runs on the command line and tests the filter with strings supplied by hand.

```php
<?php
// filter.php — what filter_var returns for valid and invalid input
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        default => get_debug_type($value) . ' ' . $value,
    };
}

$measurements = [
    'valid number'          => filter_var('12', FILTER_VALIDATE_INT),
    'invalid form number'   => filter_var('12a', FILTER_VALIDATE_INT),
    'null_on_failure'       => filter_var('12a', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE),
];

foreach ($measurements as $name => $result) {
    printf("%-24s%s\n", $name, show($result));
}
```

```
valid number            int 12
invalid form number     false
null_on_failure         null
```

For `12` the function returns `int 12`: the string has been converted to an integer. For `12a` it returns `false`. The function raised no warning, threw no exception, and execution went on; the problem is heard only by code that looks at the return value. This is the **return** channel of the course: the signal does not arrive by itself, and the caller has to ask for the value. **RR8.** None of the functions in this measurement produces an `E_WARNING`; that is not a setting but the defined behavior of `filter_var`, which is why no error handler was installed for the measurement.

The third row shows that the `FILTER_NULL_ON_FAILURE` flag turns failure into `null` instead of `false`. The channel does not change, it is still the return value; only the value itself differs. The meaning of the flag becomes clear in a later section: `false` is a valid result for the Boolean filter, and the flag exists to separate it from failure.

#### Sending the Form to the Server

A real form submission is measured with the built-in server. The script reads two fields: `age` must be an integer, and `name` is user text that is written to the output after escaping.

```php
<?php
// form.php — validates the age sent by POST, writes the name escaped
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT);
$name = $_POST['name'] ?? '';

if ($age === false) {
    echo "invalid age\n";
} else {
    echo 'age=', $age, "\n";
}
echo 'name=', htmlspecialchars($name), "\n";
```

```bash
# server.sh — sends the form to the built-in server
php -S 127.0.0.1:8082 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8082/form.php && break; sleep 0.1; done
echo '--- valid'
curl -s -d 'age=30' --data-urlencode 'name=Ayse' -w 'status=%{http_code}\n' http://127.0.0.1:8082/form.php
echo '--- invalid age'
curl -s -d 'age=12a' --data-urlencode 'name=Ayse' -w 'status=%{http_code}\n' http://127.0.0.1:8082/form.php
echo '--- name with tags'
curl -s -d 'age=30' --data-urlencode 'name=<b>Ayse</b>' -w 'status=%{http_code}\n' http://127.0.0.1:8082/form.php
kill $pid
```

```
--- valid
age=30
name=Ayse
status=200
--- invalid age
invalid age
name=Ayse
status=200
--- name with tags
age=30
name=&lt;b&gt;Ayse&lt;/b&gt;
status=200
```

The valid submission printed `age=30`. In the invalid submission the script printed `invalid age`, yet the status code is still **200**: from the server's point of view the request was handled successfully, because the validation result is not an error for PHP but a decision the code makes for itself. Reporting a validation failure to the client depends on the code choosing a status code or a message; PHP does not do it on its own. **RR9.** The status code does not say that a `false` return occurred; seeing 200 does not mean the validation passed.

In the third submission the tag in the `name` field was written as `&lt;b&gt;Ayse&lt;/b&gt;` thanks to `htmlspecialchars`. Without escaping, the browser would interpret this text as markup. This function is silent on the return channel as well: no channel reports that the value contains a tag.

#### Filter Options Narrow the Range

A type check is often not enough: someone who types `150` or `-5` into the age field has sent a valid integer but not a valid age. `filter_var` solves this with the `options` array in the third argument; the `min_range` and `max_range` keys set the accepted range. The filter also trims whitespace at the start and end of the value, so a value such as `' 30 '` from a form counts as valid.

```php
<?php
// range.php — range and whitespace behavior of the integer filter
$options = ['options' => ['min_range' => 0, 'max_range' => 120]];

$inputs = ['30', ' 30 ', '150', '-5', '3.0'];
foreach ($inputs as $input) {
    $result = filter_var($input, FILTER_VALIDATE_INT, $options);
    printf("%-8s%s\n", '[' . $input . ']', $result === false ? 'false' : 'int ' . $result);
}
```

```
[30]    int 30
[ 30 ]  int 30
[150]   false
[-5]    false
[3.0]   false
```

The values `150` and `-5`, which fall outside the range, follow the same path as `12a`: both return `false`. From the caller's side, three different problems, a format error, exceeding the upper bound and falling below the lower bound, are reduced to one value. To find out which one occurred, the code has to examine the input again; the return channel says only that a failure happened and does not carry the cause. `3.0` is also rejected, because the filter does not accept decimal notation as an integer. **RR10.** The trimming and decimal behavior shown here belongs to the PHP 8 family in which the measurement ran; the lesson does not trust default limits and passes the range on every call.

#### The Boolean Filter Separates `false` from Failure

The real job of the `FILTER_NULL_ON_FAILURE` flag shows up in the Boolean filter. For `FILTER_VALIDATE_BOOLEAN`, `false` is a valid result: `"false"`, `"0"` and the empty string are accepted spellings of it. Without the flag, both a valid `false` and an unrecognized text return `false`, and the caller cannot tell them apart. With the flag, the unrecognized text becomes `null` and the distinction stays in the value itself.

```php
<?php
// boolean.php — separating a valid false from failure in the Boolean filter
function show(mixed $value): string
{
    return $value === null ? 'null' : ($value ? 'true' : 'false');
}

foreach (['true', 'on', 'false', '0', '', 'abc'] as $input) {
    $withoutFlag = filter_var($input, FILTER_VALIDATE_BOOLEAN);
    $withFlag = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
    printf("%-8s withoutflag=%-6s withflag=%s\n", '[' . $input . ']', show($withoutFlag), show($withFlag));
}
```

```
[true]   withoutflag=true   withflag=true
[on]     withoutflag=true   withflag=true
[false]  withoutflag=false  withflag=false
[0]      withoutflag=false  withflag=false
[]       withoutflag=false  withflag=false
[abc]    withoutflag=false  withflag=null
```

In the column without the flag, `false`, `0`, the empty string and `abc` look the same; only `abc` is actually invalid. The flag returns `null` for `abc`, and the others remain a valid `false`. This is the claim that was mentioned in earlier sections but not measured: the flag does not move failure to a new channel, it opens a distinction between `false` and `null` within the existing return channel. **RR11.** To use this distinction, the caller has to test with `=== null`; the not operator still treats `null` and `false` the same.

#### A Missing Field Looks Like an Invalid Value

One trap of validation is a field that was never sent. The earlier `form.php` turns a missing field into the empty string with `?? ''`, and the filter rejects the empty string; the result is `false`, as if `12a` had been sent. To tell them apart, the presence of the key is tested separately.

```php
<?php
// missing_field.php — a missing field and an invalid field land on the same false
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT);
$present = array_key_exists('age', $_POST) ? 'yes' : 'no';
echo 'filter=', $age === false ? 'false' : $age, ' field_present=', $present, "\n";
```

```bash
# missing_field.sh — sends two POST requests, one with the field and one without
php -S 127.0.0.1:8082 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8082/missing_field.php && break; sleep 0.1; done
echo '--- age=12a'
curl -s -d 'age=12a' -w 'status=%{http_code}\n' http://127.0.0.1:8082/missing_field.php
echo '--- no field'
curl -s -d 'name=Ayse' -w 'status=%{http_code}\n' http://127.0.0.1:8082/missing_field.php
kill $pid
```

```
--- age=12a
filter=false field_present=yes
status=200
--- no field
filter=false field_present=no
status=200
```

The filter result is the same in both requests: `false`. Only `array_key_exists` shows the difference. In the Superglobals lesson the missing field was on the warning channel; `?? ''` pulled it to the silent channel, and validation finally lowered it to the return channel. Three channels are chained in a single line, and at no point does the result say which one is in effect. **RR12.** Code that does not know which channel was chosen meets "the field is missing" and "the field is malformed" with the same error message.

#### Other Filters Use the Same Return Path

The integer and Boolean filters are not the only examples. The email and decimal number filters work the same way: they return the value for valid input and `false` for invalid input. For decimal numbers the separator is taken to be a period; the input of a user who types a comma is rejected.

```php
<?php
// others.php — return values of the email and decimal number filters
$measurements = [
    'email valid'     => filter_var('name@example.test', FILTER_VALIDATE_EMAIL),
    'email invalid'   => filter_var('name@', FILTER_VALIDATE_EMAIL),
    'decimal point'   => filter_var('1.5', FILTER_VALIDATE_FLOAT),
    'decimal comma'   => filter_var('1,5', FILTER_VALIDATE_FLOAT),
];

foreach ($measurements as $name => $result) {
    printf("%-18s%s\n", $name, $result === false ? 'false' : get_debug_type($result) . ' ' . $result);
}
```

```
email valid       string name@example.test
email invalid     false
decimal point     float 1.5
decimal comma     false
```

All four filters speak the same way: the signal is in the return value, and there is no detail. The rejection of `1,5` shows that the filter does not look at the locale; changing the separator is the caller's job. **RR13.** All the measurements of this lesson reach one conclusion: PHP does not report the invalidity of the input to the client, it gives the caller only a value, and what follows is the code's decision.

#### The Bounding Measurement

The weak point of the return channel is how failure is tested. The shortest way to tell failure from `false` is to test the value with the not operator, but `0` is a valid integer.

```php
<?php
// zero.php — a valid 0 and a failure tested the same way
$measurements = [
    'zero'    => filter_var('0', FILTER_VALIDATE_INT),
    'invalid' => filter_var('0a', FILTER_VALIDATE_INT),
];

foreach ($measurements as $name => $result) {
    $short = !$result ? 'judged failed' : 'valid';
    $strict = $result === false ? 'failed' : 'valid';
    printf("%-10s%s: %-18s%s: %s\n", $name, 'not operator', $short, 'strict', $strict);
}
```

```
zero      not operator: judged failed     strict: valid
invalid   not operator: judged failed     strict: failed
```

For `0`, `filter_var` returns a valid `int 0`. When tested with the not operator, this value looks the same as `false`, and valid input is counted as failed. The strict comparison (`=== false`) separates the two. **RR14.** This is not a PHP quirk but the natural limit of the return channel: the channel fits both the result and the failure into a single value, and the caller makes the distinction. The `FILTER_NULL_ON_FAILURE` flag moves the distinction into the value itself, but then a valid `false` and `null` are what get separated for the Boolean filter; in every case, code that reads the signal has to know which value means what.

#### Summary

- Form data arrives as `string` and `filter_var` tests it with a filter: a valid value comes back in the appropriate type, an invalid one comes back as `false`.
- The channel measured in this lesson is **return**: for the invalid `12a` there is neither a warning nor an exception; the problem is heard only by code that looks at the return value.
- A validation failure does not change the status code: even if the script prints `invalid age` by its own decision, the response stays 200.
- `htmlspecialchars` converts tags to `&lt;` and `&gt;`; the classification of escaping belongs to M25.
- A valid `0` looks the same as failure under the not operator; the strict comparison (`=== false`) separates the two.

#### Next Step

In this lesson the request carried a single value, and a missing or malformed field was caught by a check. In a file upload, the request carries the file itself and a status code that accompanies it. The File Uploads lesson measures what `move_uploaded_file` returns when it is asked to move a file that was not uploaded, and what is left in `$_FILES` and `$_POST` when the server limit is exceeded.

### File Uploads

Source: https://academia.sh/en/courses/php-fundamentals/file-uploads

How a file upload reports trouble through return values and error codes: move_uploaded_file on a file that was not uploaded, exceeding a server limit, and a request emptied by exceeding post_max_size.

In the Form Handling lesson an invalid value came back from the `filter_var` function as `false`, and the request went on with a 200. A file upload uses the same channel, but the request has two parts: the file itself and the status information PHP attaches to it. The question widens as well: when an upload fails, where is that information, and what happens if it never arrives?

M04 covered the HTTP side of an upload, the structure of the multipart body, and M16/K01 covered how a server handles a request; here what is measured is what PHP extracts from that request. An uploaded file is placed in the `$_FILES` superglobal; each file field has the keys `name` (the name the client gave), `size` (in bytes), `error` (a status code) and `tmp_name`, which holds the path of the temporary file. Because the temporary path depends on the environment, it is not printed in any output of this lesson.

#### A Non-Uploaded File Is Heard Through the Return

The only way to move a file to a permanent location is the `move_uploaded_file` function, and it works only for a file that PHP itself received in that request. The first measurement runs on the command line; no upload has taken place there, so the function should fail even when applied to an ordinary file that exists.

```php
<?php
// channel_upload.php — which channel is heard when a file that was not uploaded is moved
$warnings = [];
set_error_handler(function (int $level) use (&$warnings): bool {
    $warnings[] = $level === E_WARNING ? 'E_WARNING' : 'E_' . $level;
    return true;
});

file_put_contents('source.txt', "hello\n");

try {
    $result = move_uploaded_file('source.txt', 'target.txt');
    $channel = $warnings ? 'warning' : ($result === false ? 'return' : 'silent');
} catch (Throwable $error) {
    $channel = 'halts';
}
printf("%-24s%-10s%s\n", 'non-uploaded file', $channel, 'result ' . var_export($result, true));
printf("%-24s%s\n", 'target created', file_exists('target.txt') ? 'yes' : 'no');
```

```
non-uploaded file       return    result false
target created          no
```

The function returns `false`; no warning, no exception. The file was not moved to the target. This is the **return** channel, and it is the channel measured in this lesson: execution goes on as if nothing happened unless the caller asks about the `false` value. **RR15.** The measurement shows that the function working only on files PHP received as part of a request is a deliberate security feature: even if the caller supplies a file name by hand, the function does not move it. Why this feature exists and how it is classified is the subject of M24 and M25.

#### The Structure of an Uploaded File

A real upload is measured with the built-in server and `curl -F`. The `-F` flag sends the request with a multipart body, and the `@` prefix makes the field's value be read from a file.

```php
<?php
// upload.php — reports the uploaded file and the field counts
$names = [
    UPLOAD_ERR_OK => 'UPLOAD_ERR_OK',
    UPLOAD_ERR_INI_SIZE => 'UPLOAD_ERR_INI_SIZE',
    UPLOAD_ERR_NO_FILE => 'UPLOAD_ERR_NO_FILE',
];

echo 'files=', count($_FILES), ' post=', count($_POST), "\n";
foreach ($_FILES as $field => $file) {
    echo $field, ' name=', $file['name'], ' size=', $file['size'];
    echo ' error=', $names[$file['error']] ?? $file['error'], "\n";
    if ($file['error'] === UPLOAD_ERR_OK) {
        $moved = move_uploaded_file($file['tmp_name'], 'stored.txt');
        echo 'moved=', $moved ? 'true' : 'false', ' size=', filesize('stored.txt'), "\n";
    }
}
```

```bash
# server.sh — uploads a small file to the built-in server
printf 'hello, world.\n' > report.txt
php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/upload.php && break; sleep 0.1; done
curl -s -F 'file=@report.txt' -F 'title=first' -w 'status=%{http_code}\n' http://127.0.0.1:8083/upload.php
kill $pid
```

```
files=1 post=1
file name=report.txt size=14 error=UPLOAD_ERR_OK
moved=true size=14
status=200
```

`$_FILES` saw one file field, and `$_POST` saw the text field (`title`). The field name, the file name the client gave, the size in bytes and the `UPLOAD_ERR_OK` error code arrived; this time `move_uploaded_file` returned `true`, because the file was part of this request. This is the only case in which the return value is `true`: the function accepts a file that came by the right path at the start of execution. **RR16.** `tmp_name` is not in this output; its value changes on every run and the lesson does not print it.

#### The `$_FILES` Structure and a Field with No File Selected

So far only three keys were read. The whole structure of a file field, with its key names, can be seen with a single script; the `tmp_name` value is not printed, only the key name is listed. The same script also measures the case where the form has a file field but the user selected no file: in this case the browser sends the field with an empty file name.

```php
<?php
// structure.php — all keys and error codes of the $_FILES fields
foreach ($_FILES as $field => $file) {
    $keys = array_keys($file);
    sort($keys);
    echo $field, ' keys=', implode(',', $keys), "\n";
    echo $field, ' error=', json_encode($file['error']), ' size=', json_encode($file['size']), "\n";
}
echo 'file_present=', isset($_FILES['file']) ? 'yes' : 'no', "\n";
```

```bash
# structure.sh — a filled field and a field with no file selected
printf 'welcome\n' > report.txt
php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/structure.php && break; sleep 0.1; done
echo '--- filled field'
curl -s -F 'file=@report.txt' http://127.0.0.1:8083/structure.php
echo '--- no file selected'
curl -s -F 'file=@/dev/null;filename=' http://127.0.0.1:8083/structure.php
kill $pid
```

```
--- filled field
file keys=error,full_path,name,size,tmp_name,type
file error=0 size=8
file_present=yes
--- no file selected
file keys=error,full_path,name,size,tmp_name,type
file error=4 size=0
file_present=yes
```

The filled field gives `error` 0 and `size` 8; in the measured PHP 8.5 release the key list contains `full_path`, `name`, `size`, `tmp_name`, `type` and `error`. **RR17.** The `full_path` key has existed since PHP 8.1; the lesson shows the key list only for the measured release. In the field with no file selected, `error` is 4, the value of `UPLOAD_ERR_NO_FILE`, and `size` is 0. This is the case where the `$_FILES['file']` key exists but no upload took place: `isset` reports it as `yes`, so the existence of the key does not show that the upload succeeded. Success is recognized by `error` being `UPLOAD_ERR_OK`.

#### A Multiple-File Array Is Per Key, Not Per Field

When the field name is given as `file[]`, PHP does not create a separate record for each file. Instead it holds the array of files under each key (`name`, `size`, `error`); that is, the structure looks not like one record per file but like one array per key.

```php
<?php
// multiple.php — the structure of a file[] field in $_FILES
$file = $_FILES['file'];
echo 'name_type=', get_debug_type($file['name']), ' count=', count($file['name']), "\n";
foreach ($file['name'] as $index => $name) {
    echo $index, ' ', $name, ' size=', $file['size'][$index], ' error=', $file['error'][$index], "\n";
}
```

```bash
# multiple.sh — uploads two files under a single field name
printf 'one\n' > a.txt
printf 'two two\n' > b.txt
php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/multiple.php && break; sleep 0.1; done
curl -s -F 'file[]=@a.txt' -F 'file[]=@b.txt' -w 'status=%{http_code}\n' http://127.0.0.1:8083/multiple.php
kill $pid
```

```
name_type=array count=2
0 a.txt size=4 error=0
1 b.txt size=8 error=0
status=200
```

`name` becomes an `array`, and the information about each file is read with the same index number from three separate arrays. A `$file['name']` read written for a single file tries to use an array as a string in a multiple upload; this is the same mistake as the `strlen` and array measurement in the Superglobals lesson. **RR18.** Because the client can change the structure by making the field name `file[]`, the structure is still the client's choice for code that expects a single file.

#### A Missing Target Path Makes the Move Raise a Warning

The move function can return `false` for two separate reasons: the file is not part of this request, or the target cannot be written. The first is silent. The second gives the same return value together with a warning. The script below receives a real upload and tries to move the file to a directory that does not exist; the text of the warning is not printed, only the level names.

```php
<?php
// target.php — an attempt to move the uploaded file to a directory that does not exist
$levels = [];
set_error_handler(function (int $level) use (&$levels): bool {
    $levels[] = $level === E_WARNING ? 'E_WARNING' : 'E_' . $level;
    return true;
});

$result = move_uploaded_file($_FILES['file']['tmp_name'], 'missing_dir/target.txt');
echo 'result=', var_export($result, true), ' warnings=', implode(',', $levels), "\n";
```

```bash
# target.sh — sends the file, the move goes to the missing directory
printf 'welcome\n' > report.txt
php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/target.php && break; sleep 0.1; done
curl -s -F 'file=@report.txt' -w 'status=%{http_code}\n' http://127.0.0.1:8083/target.php
kill $pid
```

```
result=false warnings=E_WARNING,E_WARNING
status=200
```

The function returns `false` and in addition reports two `E_WARNING`s: one says the target could not be opened, the other that the move failed. The same return value had come without a warning for the file that was not uploaded in the first section; here the channel shifts to **warning**. **RR19.** This difference is invisible to code that looks at the return value: both are `false`. Only code that has an error handler or a log file can tell why the move failed; the status code is again 200.

#### When a Server Limit Is Exceeded

PHP limits uploads with two ini values: `upload_max_filesize` is the upper bound for a single file, and `post_max_size` is the upper bound for the whole request. The lesson trusts no default setting; the limit is passed with `-d`. In the first measurement the single-file limit is kept small and a large file is sent. In the second measurement the file field is not sent at all.

```bash
# limit.sh — exceeding the file limit and not sending the file field
head -c 3000 /dev/zero | tr '\0' 'a' > large.txt
php -S 127.0.0.1:8083 -d display_errors=0 -d upload_max_filesize=1K >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/upload.php && break; sleep 0.1; done
echo '--- 3000 bytes, limit 1K'
curl -s -F 'file=@large.txt' -F 'title=second' -w 'status=%{http_code}\n' http://127.0.0.1:8083/upload.php
echo '--- no file field'
curl -s -F 'title=third' -w 'status=%{http_code}\n' http://127.0.0.1:8083/upload.php
kill $pid
```

```
--- 3000 bytes, limit 1K
files=1 post=1
file name=large.txt size=0 error=UPLOAD_ERR_INI_SIZE
status=200
--- no file field
files=0 post=1
status=200
```

When the limit is exceeded, the field is still in `$_FILES`: its name is kept, `size` becomes 0 and `error` becomes `UPLOAD_ERR_INI_SIZE`. This time the signal arrives in the error code; if the code does not read it, execution goes on as if the file had never come. The request again gives 200. When the file field is not sent at all, `$_FILES` is empty and `$_POST` is filled: code that reads `$_FILES['file']` directly gets both a warning and `null`, which is the missing key of the Superglobals lesson.

When storing a file, one should not put the name the client gave directly into a path, should not infer the type from the extension alone, and should not place the permanent location under the document root; the reasoning behind these principles is covered in M16/K01, and their classification and defense procedures in M24 and M25. In this lesson `stored.txt` was used as a fixed name only to make the measurement repeatable.

#### The Bounding Measurement

In the previous section there was a field that came back with an error code. If the limit is pulled down further, even that is gone. When `post_max_size` is exceeded, PHP does not parse the body of the request at all: neither the file nor the text field is left in hand.

```bash
# ceiling.sh — what is left when post_max_size is exceeded
head -c 5000 /dev/zero | tr '\0' 'a' > large.txt
php -S 127.0.0.1:8083 -d display_errors=0 -d post_max_size=1K >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/upload.php && break; sleep 0.1; done
curl -s -F 'file=@large.txt' -F 'title=fourth' -w 'status=%{http_code}\n' http://127.0.0.1:8083/upload.php
kill $pid
```

```
files=0 post=0
status=200
```

Both `$_FILES` and `$_POST` are empty, and the status code is 200. There is not even a field carrying an error code: the request cannot be told apart from a request that sent no fields at all. **RR20.** It was measured with `display_errors=0`; with the setting on, a warning PHP produces at startup may fall into the body, and the lesson does not print that text. This is the lower bound of the channel in the previous section: the return channel works when the value the channel would speak about has arrived. When the value never arrives, there is no error code left to read. The only way for the code to notice this is to test the absence of the fields it expects as a separate condition.

#### Summary

- `$_FILES` carries `name`, `size`, `error` and `tmp_name` for each file field; `move_uploaded_file` moves only a file that this request received.
- The channel measured in this lesson is **return**: for a file that was not uploaded, `move_uploaded_file` returns `false` without a warning.
- When `upload_max_filesize` is exceeded, the field stays in `$_FILES` with `size` 0 and `error` `UPLOAD_ERR_INI_SIZE`; the request still gives 200.
- If the file field is not sent, `$_FILES` stays empty and `$_POST` stays filled.
- When `post_max_size` is exceeded, both `$_POST` and `$_FILES` are emptied; not even a field carrying an error code is left, and the request looks like an empty request without a signal.

#### Next Step

In the upload lesson the code gave a script such as `upload.php` a file it had written itself and got a response. A script pulling another file into itself is a separate operation and brings a new form of the same question: what happens if the file to be pulled in cannot be found? The Including Files lesson measures through which channel the inclusion of a missing file is heard and how that channel changes with the kind of include.

### Including Files

Source: https://academia.sh/en/courses/php-fundamentals/including-files

Through which channel the inclusion of a missing file is heard with include, require and include_once, followed by a course wrap-up that gathers the measurements of fourteen lessons in one table.

The File Uploads lesson ended with a file that arrived with the request being handed to a function that returns `false`. A script pulling another file into itself asks the same question through a different mechanism: if the file to be pulled in does not exist, through which channel is that heard? The answer changes with the form of inclusion used. The lesson measures the fourteenth observation of the core and its counterparts; then it gathers the whole course in a single table.

The measurement again uses the course core, the `channel.php` file. Because every lesson runs in its own working directory, the file is given verbatim here as well.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

#### A Missing File Raises a Warning with `include`

The first script measures three things: pulling in a file that does not exist with `include`, pulling in a file that does exist twice with `include_once`, and how many times that file ran. The script writes the existing file itself, so the measurement does not depend on a file left over from an earlier run.

```php
<?php
// includes.php — which channel is heard for a missing file and for include_once
require 'channel.php';

file_put_contents('part.php', "<?php\n\$GLOBALS['loaded'] = (\$GLOBALS['loaded'] ?? 0) + 1;\nreturn 7;\n");

$measurements = [
    'missing include file' => fn() => include 'missing.php',
    'first include_once'   => fn() => include_once 'part.php',
    'second include_once'  => fn() => include_once 'part.php',
];

foreach ($measurements as $name => $measurement) {
    [$channel, $evidence] = listen($measurement);
    printf("%-24s%-10s%s\n", $name, $channel, $evidence);
}
echo 'part.php run count ', $GLOBALS['loaded'], "\n";
```

```
missing include file    warning   E_WARNING, result false
first include_once      silent    result int 7
second include_once     silent    result true
part.php run count 1
```

The first row is the core's `missing include file` observation: for the file it could not find, `include` reported an `E_WARNING`, returned `false` and execution went on. This is the **warning** channel, and it is the channel measured in this lesson. **RR21.** The returned `false` is there too, but the order in which `listen()` asks decides the channel: a warning is asked about before a return. That is why the row says **warning**, not **return**.

The next two rows are for `include_once`. The first call runs the file and gives the `7` that the file returns. The second call does not run the file again and returns only `true`; the last line confirms that the file ran once in total. **RR22.** This is the same behavior for `include_once` and `require_once`, and it prevents clashes such as defining the same function twice. But the second call giving `true` instead of `7` is a silent difference: code that uses the file's return value gets a different value on the second call.

#### Require Halts After the Warning

`require` meets the same absence more harshly. It leaves no `false` for execution to carry on with; if the file is not found, it stops. In what form it does so is measured, not assumed. Two measurements are made: in the same process with a `try`, and in a separate execution through the exit code.

```php
<?php
// required.php — which channel is heard for a missing file with require
require 'channel.php';

[$channel, $evidence] = listen(fn() => require 'missing.php');
printf("%-24s%-10s%s\n", 'missing require', $channel, $evidence);
echo 'warning heard first: ', implode(',', $GLOBALS['heard_warnings']), "\n";

[$channel, $evidence] = run_separately('echo "before\n"; require "missing.php"; echo "after\n";');
printf("%-24s%-10s%s\n", 'separate run', $channel, $evidence);
```

```
missing require         halts     Error
warning heard first: E_WARNING
separate run            halts     lines out 1, exit 255
```

**RR23.** The channel is **halts**: `require` threw a `Throwable` and the separate execution exited with code 255, and the `after` line was not printed. The channel gave more than one signal: `require` first reported an `E_WARNING`, then cut execution. The core's question picks the strongest channel, so the channel of the row is halts, not warning. This is the shared rule of the course: if two channels speak in the same event, the measure is the channel that cuts execution.

#### Does the Path Depend on the Working Directory

A relative path may depend on the current working directory, not on the script's own directory. When the same file is given with `__DIR__`, this dependency goes away. `__DIR__` is the path of the directory where the running file sits; its value depends on the environment and is not printed in this lesson, only whether the path built with it works.

```php
<?php
// path.php — whether the included path depends on the working directory
require 'channel.php';

file_put_contents('part.php', "<?php\nreturn 7;\n");
chdir('/');

$measurements = [
    'relative ./ path' => fn() => include './part.php',
    '__DIR__ path'     => fn() => include __DIR__ . '/part.php',
];

foreach ($measurements as $name => $measurement) {
    [$channel, $evidence] = listen($measurement);
    printf("%-24s%-10s%s\n", $name, $channel, $evidence);
}
```

```
relative ./ path        warning   E_WARNING, result false
__DIR__ path            silent    result int 7
```

**RR24.** After the working directory changed, the path that begins with `./` could not find the file and raised a warning; the path built with `__DIR__` was found, and the file's `7` came back. If the path ties the location of the included file to the location of the script, `__DIR__` is used. This does not change the channel of the error; it only reduces the chance of the error occurring.

#### The Bounding Measurement

The warning channel of `include` depends on a condition: it applies when the file is **not found**. If the file is found and its syntax is broken, the situation changes. The script below pulls in a broken file with `include`.

```php
<?php
// broken.php — how include is heard for a file that is found but has broken syntax
require 'channel.php';

file_put_contents('broken_part.php', "<?php\n\$x = ;\n");

[$channel, $evidence] = listen(fn() => include 'broken_part.php');
printf("%-24s%-10s%s\n", 'broken included file', $channel, $evidence);
```

```
broken included file    halts     ParseError
```

**RR25.** This time `include` threw a `ParseError`, and the channel is **halts**. Finding the file turned the channel from warning into halts. This is the measured form of the summary in the Syntax and Tags lesson: a syntax error in an included file is thrown, after the main file has started running, as a catchable `ParseError`. As a result, the sentence "an error does not cut execution" is not true for `include`; the true sentence is "a missing file does not cut execution".

#### Summary

- A file that cannot be found makes `include` report an `E_WARNING` and return `false`; execution continues. The channel measured in this lesson is **warning** (`missing include file`).
- For the same absence, `require` first reports a warning and then cuts execution; its channel is **halts**.
- On the second call, `include_once` does not run the file and returns `true`; the return value of the first call is lost.
- A path that begins with `./` depends on the working directory; a path built with `__DIR__` depends on the location of the script.
- If the file is found but its syntax is broken, `include` also throws a `ParseError` and halts.

#### Course Wrap-Up

One question was asked throughout the course: through which channel is this problem heard? Fourteen lessons each measured a mechanism, and each named its channel with one of these four names. The table shows, on each row, the mechanism the lesson measured, its channel and the place where that channel ends.

| Lesson | Mechanism measured | Channel | Bounding measurement |
|---|---|---|---|
| The PHP Execution Model | core with fourteen observations | halts | on a server every request starts from the beginning |
| Syntax and Tags | parse error versus execution error | halts | a syntax error cuts execution before it starts |
| Variables and Data Types | type depends on value | silent | `strict_types` binds the caller's file |
| Operators | numeric-prefix string | warning | `??` silences the warning |
| Control Flow | unmatched `match` | halts | the `default` arm turns halting into silence |
| Functions | extra argument | silent | a built-in function throws `ArgumentCountError` |
| Indexed and Associative Arrays | key normalization | silent | a float key gives `E_DEPRECATED` |
| Multidimensional Arrays | missing inner key | warning | `??` silences the whole path |
| Array Functions | `array_search` finds nothing | return | `0` becomes `false` in a loose comparison |
| String Handling | malformed JSON | return | `JSON_THROW_ON_ERROR` halts |
| Superglobals | missing query key | warning | an array arrives and a `TypeError` is thrown, the status code changes |
| Form Handling | invalid form number | return | a valid `0` looks the same as failure |
| File Uploads | non-uploaded file | return | exceeding `post_max_size` empties both arrays |
| Including Files | missing include file | warning | broken syntax gives `ParseError` |

The distribution over the fourteen lessons is 3 silent · 4 return · 4 warning · 3 halts; because the core carries one observation for each lesson, the distribution of the core is the same. The four channels are four different costs of the same fault. In the silent channel the wrong result pays the cost, in return the caller's attention, in warning the eye that reads the notice, and in halts the running execution.

The table has a second reading: the last column of each row shows that the channel depends on a condition. `??` turns a warning into silence, `JSON_THROW_ON_ERROR` moves a return into halts, and the `default` arm turns halting into silence. A channel is not a fixed property of the mechanism but a property that shifts with the code the caller writes. For that reason, instead of assuming the channel, the same question is asked for every new mechanism.

With this question the course separated the ways PHP lets problems be heard; the security consequences of these classifications are the subject of M24 and M25. The next course covers classes, interfaces and namespaces in PHP.

