In PHP, what steps turn a .php source file into running code, and which of them repeat on every request?
answer
- tokens, then a syntax tree
- AST since PHP 7
- op_array of opcodes
- Zend VM executes opcodes
- no opcode cache means recompiling
basics
~20 sPHP tokenizes the source, parses the tokens into an abstract syntax tree, compiles the tree into opcodes and runs them on the Zend virtual machine. Without an opcode cache, every request repeats all four steps for every file it loads.
solid answer
~40 sPHP is compiled, but to **opcodes** for its own virtual machine, not to native code. For each file the engine: **lexes** the source into tokens, **parses** them into an **abstract syntax tree** (the AST, introduced in PHP 7), **compiles** the AST into an `op_array` of opcodes such as `ZEND_ADD` or `ZEND_ECHO`, then the **Zend VM executes** those opcodes. Included files go through the same pipeline when `include` or `require` reaches them. Because of the share-nothing model, the compiled opcodes belong to the request unless an opcode cache keeps them, so a plain setup recompiles every file on every request. Syntax errors are found at compile time, which is why a `ParseError` stops the file before any of its lines run.
code
php · 12 lines<?php
declare(strict_types=1);
$tokens = PhpToken::tokenize('<?php echo $a + 1;');
foreach ($tokens as $token) {
echo $token->getTokenName(), ' ', json_encode($token->text), PHP_EOL;
}
// T_OPEN_TAG "<?php "
// T_ECHO "echo"
// T_WHITESPACE " "
// T_VARIABLE "$a"
// ...go deeper
Know that PHP compiles each file to opcodes before running it, which is why one syntax error stops the whole file.
Walk through lexing, parsing to an AST, compiling to an op_array and executing on the Zend VM, and say which stages an opcode cache skips.
Connect compilation cost to request latency: hundreds of framework files per request are compiled every time unless cached, and caching code is not caching state.
Weigh where per-request compile and bootstrap cost matters for a fleet, and when caching compiled code is enough versus changing the runtime model.
## PHP is compiled, just not ahead of time A common junior belief is that PHP is "interpreted line by line". In fact the engine, called the **Zend Engine**, compiles each source file into an intermediate instruction set before running any of it. Those instructions are **opcodes**, and the program that runs them is the **Zend virtual machine**. ## The four stages 1. **Lexing (scanning).** The scanner reads the raw text and emits **tokens**: `T_VARIABLE` for `$total`, `T_FUNCTION` for `function`, and so on. You can see this stage from user land with `token_get_all()` or `PhpToken::tokenize()`. 2. **Parsing.** The parser checks the token stream against PHP's grammar and builds an **abstract syntax tree (AST)**, a tree of nodes such as "function call", "binary +" and "assignment". PHP 7 introduced this AST step; earlier versions emitted opcodes straight from the parser. 3. **Compilation.** The compiler walks the AST and emits an **`op_array`**, a flat list of opcodes with their operands, one per function or method plus one for the file's top-level code. Examples of opcodes are `ZEND_ADD`, `ZEND_ECHO` and `ZEND_RETURN`. 4. **Execution.** The Zend VM steps through the `op_array`, running each opcode's handler. | Stage | Input | Output | Typical error found here | |---|---|---|---| | Lexing | source text | tokens | an unterminated string or comment | | Parsing | tokens | AST | `ParseError` for a missing `;` or brace | | Compilation | AST | opcodes | compile-time fatal errors, such as redeclaring a class member | | Execution | opcodes | effects, output | `TypeError`, `Error`, warnings | ## Why syntax errors stop the whole file Because a file is compiled **before** it runs, a syntax error anywhere in it prevents every line in that file from executing, including the lines above the error. A `ParseError` raised while compiling an included file is thrown at the `include` or `require` that loaded it, so the including script can catch it, but the broken file itself runs nothing. ## Files are compiled when they are reached The entry script is compiled first. Other files are compiled when execution reaches the `include`, `require` or autoloader call that loads them, so a request that never touches a class never pays to compile its file. Top-level function and class declarations inside a compiled file can be hoisted, which is why a function defined at the bottom of a file can be called at the top. ## What repeats on every request In the share-nothing model, the `op_array`s the compiler produces are allocated as **request memory** and discarded at request shutdown. So in a plain setup the answer is **all four stages, for every file, on every request**. A framework that loads a few hundred files per request pays that compilation cost hundreds of times per page. An **opcode cache** removes the first three stages for files it has already compiled by keeping their opcodes in shared memory that outlives requests. In PHP 8.5 the OPcache extension is always built into the binary, but whether it caches is still controlled by its ini settings, and its tuning, invalidation and preloading are a separate topic. The key point for the lifecycle is what it does **not** do: it caches compiled code, not variables, objects or any other request state. ## Where the JIT fits PHP 8.0 added a JIT compiler to OPcache that can translate hot opcodes into native machine code. It sits after stage 3 and changes how some opcodes run; it does not change the share-nothing lifecycle either. ## Seeing it for yourself - `php -l file.php` runs the pipeline up to compilation and reports syntax errors without executing the file. - `PhpToken::tokenize(file_get_contents('file.php'))` returns the token objects of stage 1. - Third-party debugging extensions can dump the opcodes of stage 3; they are not part of PHP itself. ## Common misconceptions - **"PHP runs line by line."** The whole file is compiled first; only then does execution start, which is why a syntax error on the last line stops the first line. - **"PHP compiles to machine code."** It compiles to Zend VM opcodes. Native code appears only when the optional JIT is enabled, and only for hot paths. - **"Compiled code is shared between requests by default."** Only an opcode cache shares it; the compiler's own output is request memory. - **"Every project file is compiled at startup."** Only the files a request actually loads are compiled, when execution reaches them.
- Why does a syntax error on line 50 stop lines 1 to 49 from running?The whole file is compiled to opcodes before any of it executes. A parse error aborts compilation, so no opcodes exist for that file and nothing in it runs. If the file was included, the ParseError surfaces at the include or require that loaded it.
- Does an opcode cache make PHP keep state between requests?No. It keeps compiled opcodes in shared memory so later requests skip lexing, parsing and compilation. Variables, objects and statics are still created per request and freed at shutdown.
saying these in an interview costs you the question
- PHP is interpreted line by line with no compile step.
- A syntax error only affects the lines after it in the file.
- PHP compiles scripts to native machine code by default.
- An opcode cache also caches variables and objects between requests.
- Every file in the project is compiled at startup whether used or not.