How the Zend Engine Works
Trace PHP from source to opcodes to execution.
How the Zend Engine Works is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Inside the Engine
PHP is not interpreted line-by-line from source. The Zend Engine compiles your script into an intermediate representation called opcodes, then a virtual machine executes them. Understanding this pipeline explains performance, OPcache, and JIT.
This lesson walks source → tokens → AST → opcodes → VM execution.
The Pipeline
Each request to a PHP file runs through four phases:
- Lexing — source text → tokens (re2c-based scanner)
- Parsing — tokens → Abstract Syntax Tree (Bison grammar)
- Compilation — AST → opcode array (op_array)
- Execution — the Zend VM walks the opcodes
Without OPcache, the first three phases repeat on every request.
Tokenizing
The lexer turns characters into tokens like T_VARIABLE, T_ECHO, T_STRING. PHP exposes this stage through token_get_all() / PhpToken, which is exactly what tools like PHP-CS-Fixer use.
<?php
$src = '<?php $x = 1 + 2; echo $x;';
foreach (PhpToken::tokenize($src) as $tok) {
if ($tok->isIgnorable()) continue; // skip whitespace
printf("%-12s %s\n", $tok->getTokenName(), trim($tok->text));
}
?>The AST
Tokens are parsed into a tree of nodes — an assignment node whose children are a variable and a binary-op expression. PHP builds this AST internally (and nikic/php-parser reconstructs an equivalent in userland for static analysis tools like PHPStan).
<?php
// Conceptual AST for: $x = 1 + 2;
//
// AST_ASSIGN
// ├── AST_VAR ($x)
// └── AST_BINARY_OP (+)
// ├── 1
// └── 2
//
// At compile time PHP folds 1 + 2 into a literal 3
// (constant folding) before generating opcodes.
echo "AST drives opcode generation\n";
?>Opcodes
The compiler emits an op_array: a flat list of opcodes. Each opcode has an opcode number (e.g. ZEND_ADD, ZEND_ECHO, ZEND_ASSIGN) and up to two operands plus a result, each being a compiled variable (CV), temporary (TMP), or constant.
<?php
// Opcodes for: $x = 1 + 2; echo $x;
//
// line op operands result
// --- ------------ ----------------- -------
// 1 ADD 1, 2 ~0
// 1 ASSIGN $x, ~0
// 1 ECHO $x
// 1 RETURN 1
//
// ~0 is a temporary; $x is a compiled variable (CV).
echo "op_array is what OPcache stores\n";
?>Inspecting Opcodes
You can dump the generated opcodes with the VLD extension or OPcache's opcache.opt_debug_level. This reveals constant folding, dead-code elimination, and how control flow becomes JMP/JMPZ opcodes.
# Dump opcodes with VLD
php -d vld.active=1 -d vld.execute=0 script.php
# Or via OPcache optimizer debug (pre/post optimization)
php -d opcache.opt_debug_level=0x10000 script.php # before opt
php -d opcache.opt_debug_level=0x20000 script.php # after optCompiled Variables (CVs)
Local variables are not hash-table lookups in compiled code — the compiler assigns each a numbered CV slot. Accessing $x the second time is an array index, not a symbol-table search. This is a major reason locals are fast.
<?php
// Each named local gets a fixed CV slot at compile time:
// $a -> CV0 $b -> CV1 $sum -> CV2
function add(int $a, int $b): int {
$sum = $a + $b; // ADD CV0, CV1 -> CV2
return $sum; // RETURN CV2
}
echo add(2, 3) . PHP_EOL;
?>The VM Execution Loop
The executor (execute_ex) walks the op_array. Each opcode maps to a handler function; PHP can build this dispatch as a giant switch, computed gotos, or hybrid (the default, fastest on supporting compilers). The opline pointer advances; JMP opcodes move it for control flow.
<?php
// if ($n > 0) echo 'pos'; compiles roughly to:
//
// IS_SMALLER 0, $n -> ~T
// JMPZ ~T, ->L1 ; if false, skip
// ECHO 'pos'
// L1:
// RETURN 1
//
// The VM follows opline; JMPZ rewrites it conditionally.
$n = 5;
if ($n > 0) echo "pos\n";
?>Where OPcache Fits
OPcache caches the compiled op_array in shared memory, skipping lex/parse/compile on subsequent requests. It also runs an optimizer pass (constant folding, dead-code removal, opcode fusion). The VM still executes the cached opcodes each request — that's where JIT later helps.
Function Call Overhead
Calls push a new VM stack frame: opcodes INIT_FCALL, SEND_VAL/SEND_VAR per argument, then DO_FCALL. Knowing this explains why excessive tiny function calls have measurable cost and why inlining/JIT matters in hot loops.
<?php
// square($x) compiles to a call sequence:
// INIT_FCALL 'square'
// SEND_VAR $x
// DO_FCALL -> ~R
// ASSIGN $y, ~R
function square(int $x): int { return $x * $x; }
$total = 0;
for ($i = 1; $i <= 5; $i++) {
$total += square($i); // one call sequence per iteration
}
echo $total . PHP_EOL; // 1+4+9+16+25 = 55
?>Request Shutdown
After execution, PHP tears down the request: running variables freed, output flushed, then the engine resets its allocator arena. In a shared-nothing FPM model each request starts clean — which is why a fatal error in one request can't corrupt another. OPcache's shared op_arrays survive across requests; the executor state does not.
Quick Check
What exactly does OPcache store to skip recompilation?
Recap
The Zend pipeline is lex → parse → compile → execute. Source becomes tokens, then an AST, then an op_array of opcodes that the VM's executor loop runs, with locals stored in numbered CV slots and calls pushing stack frames. OPcache caches the op_array (plus an optimizer pass) so only execution repeats per request.
Frequently asked questions
Is the “How the Zend Engine Works” lesson free?
Yes — the full text of “How the Zend Engine Works” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “How the Zend Engine Works”?
Trace PHP from source to opcodes to execution. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “How the Zend Engine Works” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.