When Does the Code Run? The Request Lifecycle of a .php File
Write <?php echo date('H:i'); ?> into a file, request it, and a time appears. The question nobody answers at first is when the code ran. PHP executes on the server, per request, before any of the file's HTML reaches the browser. Trace that once and most "why did my variable disappear" confusion never happens.
Follow one request end to end:
- The browser sends
GET /hello.phpto the server. - The web SAPI — the built-in server, php-fpm, or Apache — maps the URL to
hello.phpon disk and hands it to PHP. - PHP reads the entire file and parses it into opcodes. One syntax error aborts here, before a single byte of output. This is why a parse error kills the whole page, while a runtime error mid-script can leave a half-rendered page.
- The engine executes the opcodes top to bottom. Every
echoappends bytes to the response body. - The SAPI sends headers (
200 OK,Content-Type: text/html; charset=UTF-8) followed by the accumulated body. - The worker is done. Nothing from this run — no variable, no function definition — is kept for the next request.
Everything in the file that sits outside <?php ... ?> is passed through untouched. That is the whole trick of the language: a .php file is mostly HTML with executable islands.
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Status board</h1>
<?php
$hour = (int) date('G');
$open = $hour >= 9 && $hour < 18;
?>
<p>Generated at <?= date('Y-m-d H:i') ?></p>
<p>The office is <?= $open ? 'open' : 'closed' ?></p>
</body>
</html>
Open view-source in the browser: you will see rendered HTML and not one character of PHP. $hour and $open existed for a few milliseconds inside the worker and are gone now. That server-side execution boundary is the security model of the language — credentials, queries, and business rules live in the islands and never ship — and it is also why leaking PHP source (a .php file served as a download or opened via file:// instead of through a SAPI) is treated as an incident.
Seeing the parse step fail is instructive. Delete a semicolon or a brace and request the page with development settings:
PHP Parse error: syntax error, unexpected '}' in hello.php on line 9
Nothing runs — not even the <h1> above the error — because step 3 happens before step 4.
The last piece of the cycle is speed. Parsing and compiling to opcodes happens on every request; OPcache (enabled by default in most production configs since PHP 7) keeps the compiled opcodes in shared memory keyed by file path and modification time, so steady-state requests skip straight to execution. When a deploy edits a file, the timestamp changes and OPcache recompiles it — which is why "I deployed but the old behavior persists" usually means a stale cache rather than a stale file.
Installing PHP and a Local Server gave you the server; now you have the timing model to match it. Next: how output actually gets written.
echo, print, printf: How Bytes Actually Leave PHP
PHP has two output constructs, one formatting function family, and one rule that has caused more production incidents than any of them.
echo is a language construct with no return value that accepts a comma-separated list, evaluated left to right:
echo "Order #", $id, " for ", $name, "\n";
It is the same as concatenating with . except no intermediate strings are built, which is why hand-rolled HTML templates use it: the pieces of markup stream out directly.
print takes exactly one expression and — the detail interviews probe — it returns 1:
$written = print "saving...\n"; // $written === 1
That return value is the only difference that matters in practice: echo is a statement, print is an expression, so print can legally appear where an expression is required and echo cannot. Nobody writes $x = print ... in real code, but the fact explains the language's asymmetry and shows up in one-liner puzzles.
printf and sprintf do C-style formatting; printf writes, sprintf returns the string:
printf("Order %s: %d units, total %.2f\n", $id, $qty, $total);
// Order A-1042: 3 units, total 12.75
%s string, %d integer, %f float with %.2f precision, %05d zero-padded. Use sprintf when building a string you will pass on rather than print.
The rule: PHP does not escape HTML.
echo "<p>" . $_GET['comment'] . "</p>"; // XSS if comment contains <script>
Anything echoed into markup must pass through htmlspecialchars($value, ENT_QUOTES, 'UTF-8') at the boundary. Unescaped output is historically the most exploited PHP vulnerability class, and a reviewer who sees raw interpolation into markup stops the review there.
Headers and buffering. The first byte of output commits the response headers. After that, changing the status or sending a redirect produces the classic warning:
Warning: Cannot modify header information - headers already sent by
(output started at hello.php:3) in hello.php on line 7
So session_start() and header('Location: /login') must run before any output — including stray whitespace. When you must compute before deciding on output, wrap the work in ob_start() / ob_get_clean() to buffer it silently.
For inspection there is var_dump($x), which prints type and value (string(5) "hello", int(42)), print_r for readable structure (print_r($x, true) returns the string instead of printing it — handy for logging into a file), and var_export for output that is itself valid PHP, which matters when generating config files or test fixtures. Locally you run with display_errors=On; in production that setting belongs to the log, not the page.
Two output targets beyond the response body round out the picture. In CLI scripts you write to the standard streams directly — fwrite(STDERR, "failed: $reason\n") — so a CI log can separate your diagnostics from the data your script prints to STDOUT. And for large payloads you do not have to build a string at all: readfile($path), fpassthru($fh), and echo json_encode($rows, JSON_PRETTY_PRINT) all stream bytes to the response without an intermediate variable, which is how PHP serves a 200 MB export without exhausting memory_limit.
Why $visits = $visits + 1 Does Not Count Visits
A common first script:
<?php
$visits = ($visits ?? 0) + 1;
echo "You are visitor number $visits";
It always prints 1. Not because the arithmetic is wrong, but because no variable survives the request. Each hit starts a brand-new execution with a fresh symbol table; $visits never existed before line 1 and is discarded after the echo.
Real state lives in places that outlive a request:
| Store | Typical use | Access from |
|---|---|---|
$_SESSION |
per-user cart, logged-in flag | session_start() then $_SESSION['key'] |
| Database | anything durable or relational | PDO / mysqli |
| Files | uploads, logs, simple ledges | file_get_contents / file_put_contents |
| APCu cache | hot read-only data | apcu_fetch() / apcu_store() |
And each request arrives with its inputs already assembled in superglobals — $_GET (query string), $_POST (form body), $_COOKIE, $_SERVER (method, path, headers). They are populated by the SAPI before your first line runs, which is why there is no "load the request" step in PHP: the data is simply there. A few $_SERVER keys earn permanent residency in your muscle memory:
| Key | Value for GET /search?q=php HTTP/1.1 on a form page |
|---|---|
$_SERVER['REQUEST_METHOD'] |
'GET' |
$_SERVER['QUERY_STRING'] |
'q=php' |
$_SERVER['HTTP_REFERER'] |
the page that linked here, if the browser sent one |
$_SERVER['REMOTE_ADDR'] |
client IP (behind a proxy, the proxy's — hence X-Forwarded-For) |
$_SESSION deserves one extra note because it looks like an exception: it is not. session_start() reads the session id from the PHPSESSID cookie, loads that id's stored data (a file by default) into $_SESSION, and writes it back at shutdown. The data still lives outside the process — the superglobal is only the per-request window onto it.
The other half of the per-request model is the statement terminator. A semicolon ends a statement, not a line. Two statements on one line are legal; one statement across three lines is legal until the ;. Omit it and the parser stops at the next token:
PHP Parse error: syntax error, unexpected 'echo' in hello.php on line 3
Because parsing happens for the whole file before execution, that error means nothing in the file ran — unlike languages with line-by-line evaluation, there is no partial state to clean up. PHP ships a command that checks exactly this without running anything:
php -l hello.php # No syntax errors detected in hello.php
Teams wire php -l into pre-commit hooks and CI so a parse error fails the build in seconds instead of taking down a page in production.
One more habit separates PHP files from PHP templates. In a file that is pure PHP (a class, a config, a library), omit the closing ?> tag. Everything after it — including the newline your editor adds — is output bytes. If such a file is ever required before headers are sent, that invisible newline triggers the headers-already-sent warning from the previous chapter. Templates that are mostly HTML do close their tags; code files do not.
By the time you move to Embedding PHP Inside HTML you should be able to predict, for any snippet, what bytes the browser will receive and when the code ran.
Quiz
1. A PHP file contains a syntax error on line 40, and line 3 has an echo statement. What does the browser receive?
2. What distinguishes print from echo in PHP?
3. A page increments `$counter` at the top of the script and always shows 1. Why?
4. After adding `header('Location: /login');` at line 10, the browser gets 'Cannot modify header information - headers already sent'. Lines 1-9 produced no echo. What did you miss?
Flashcards
Question
In what order does PHP process a .php file per request, and where does a parse error stop it?
Click to reveal answer
Answer
SAPI maps the URL to the file, PHP parses the WHOLE file into opcodes, executes them top to bottom, then headers plus accumulated body are sent and worker state is discarded. A parse error stops the request at the parse stage, so none of the file executes.
Question
echo vs print: the one semantic difference?
Click to reveal answer
Answer
echo is a statement with no return value that accepts multiple comma-separated arguments; print takes exactly one expression and returns 1, so it is usable as an expression. Output behavior is identical.
Question
Why must a pure-PHP file omit the closing ?> tag?
Click to reveal answer
Answer
Everything after ?> is output, including the trailing newline. When such a file is required before any output, those bytes commit the response and trigger 'Cannot modify header information - headers already sent'. Templates that are mostly HTML do close the tag; code files do not.
Question
Where does per-request input arrive in PHP, and where must durable state live?
Click to reveal answer
Answer
Input arrives pre-populated in superglobals: $_GET, $_POST, $_COOKIE, $_SERVER (and $_FILES). Durable state must live outside the process: $_SESSION (server-side storage keyed by a cookie), a database, files, or a cache such as APCu.
Question
What does OPcache store, and what invalidates an entry?
Click to reveal answer
Answer
Compiled opcodes in shared memory, keyed by file path and modification time. Saving the file changes the mtime, so the next request recompiles it. It caches compilation, not variables or responses — each request still executes the opcodes.
Revision Notes
Key Takeaways
- 1. PHP runs server-side, once per request: parse the whole file, execute top to bottom, send headers plus body, discard everything
- 2. A parse error prevents all execution of the file; a runtime error stops only after partial output
- 3. echo takes a comma list and returns nothing; print takes one expression and returns 1; printf formats with %s/%d/%f
- 4. Nothing persists between requests — counters, configs, and connections must live in sessions, a database, files, or APCu
- 5. Output commits headers, so header() and session_start() run first; pure-PHP files end without ?> to avoid stray output bytes
Interview Tips
- • "What happens when you request a PHP page?" — walk the lifecycle in order: URL-to-file mapping by the SAPI, whole-file parse to opcodes, execution with output buffering, headers plus body, process ends. Mentioning the parse-before-execute distinction is what separates a strong answer
- • "Why does my visitor counter always show 1?" — the per-request execution model; then name where state really lives (session, DB, files, APCu) without being prompted
- • "echo vs print vs printf?" — echo: statement, multiple args; print: expression returning 1, one arg; printf: C-style formatting. Add the XSS note: neither escapes HTML, so htmlspecialchars at the output boundary is mandatory
- • "You get headers already sent with no visible output — cause?" — whitespace or a closing ?> newline before the header() call; fix by removing it, or buffer with ob_start()/ob_get_clean(); mention display_errors printing a notice can itself be the early output
Cheat Sheet
Your First PHP Script
Request cycle (memorize the order):
request → SAPI maps URL to file → parse whole file into opcodes (parse error aborts here) → execute top to bottom, echo appends to body → headers + body sent → worker state discarded.
Output constructs:
echo "a", $b, "c"; // statement, multiple args, no return value
print "x"; // expression, ONE arg, returns 1
printf("%s: %d units, %.2f\n", $id, $qty, $total); // C-style formatting
var_dump($x); // type + value: string(5) "hello", int(42)
Everything echoed into HTML must go through htmlspecialchars($v, ENT_QUOTES, 'UTF-8').
Headers rule: the first output byte commits the headers. session_start() / header(...) before any output, or buffer with ob_start() + ob_get_clean(). Symptom: Cannot modify header information - headers already sent.
Statelessness: variables die with the request. Durable state lives in $_SESSION, a database, files, or APCu; per-request input arrives pre-loaded in $_GET, $_POST, $_COOKIE, $_SERVER.
Syntax:
- Every statement ends with
;— the semicolon terminates the statement, not the line. - A parse error aborts the whole file before any of it executes.
- Pure-PHP files end without
?>— bytes after it (even a newline) become output and can break headers. - OPcache serves compiled opcodes from shared memory; the file's mtime invalidates them.