Phasis
A JavaScript engine written in PHP: ES2024+ passing all 50,506 test262 tests (2025-05-05 snapshot), plus URL, encoding, structuredClone, fetch and Streams.
composer require phasis/phasisLatest release v0.4.2 MIT PHP 8.2+ with bcmath, curl and mbstring
./vendor/bin/phasis -e '1 + 2 * 3'
# 7
./vendor/bin/phasis -e '[1, 2, 3].map(x => x * x).join(",")'
# 1,4,9
./vendor/bin/phasis path/to/script.jsIn practice
One engine object runs JavaScript inside the PHP process and passes values in both directions.
Run JavaScript from PHP
Phasis\Engine evaluates source strings and files in the PHP process that calls it. setGlobal() turns PHP arrays into JavaScript arrays and objects, and wraps PHP closures as functions.
console.log output is buffered on the engine and read back with getConsoleOutput(). The formatter in the second tab needs no engine at all.
use Phasis\Engine;
$engine = new Engine();
// Evaluate an expression
$engine->eval('1 + 2 * 3'); // 7
// Run a file
$engine->execFile('/path/to/script.js');
// Bridge a PHP value into JS (arrays are copied)
$engine->setGlobal('config', ['debug' => true]);
$engine->eval('config.debug'); // true
// console.log is buffered, not printed
$engine->eval('console.log("hi")');
echo $engine->getConsoleOutput(); // hi
// Expose a PHP closure as a JS function
$engine->setGlobal('greet', fn(string $name) => "Hello, $name");
$engine->eval('greet("world")'); // "Hello, world"use Phasis\Formatter\Formatter;
use Phasis\Formatter\FormatOptions;
$formatted = Formatter::formatSource(
"const a={x:1,y:2};",
'script',
new FormatOptions(useTabs: true, singleQuote: true),
);
// "const a = { x: 1, y: 2 };\n"Pure PHP
No exec, no FFI, no Node. Requires PHP 8.2+ with ext-curl, ext-bcmath and ext-mbstring; CI runs PHP 8.4 and 8.5.
100% test262
Passes all 50,506 tests of the ECMAScript conformance suite at its 2025-05-05 snapshot, every category, no skips.
Modern ECMAScript
Classes, async/await, generators, Symbol, BigInt, Proxy, Temporal, TypedArray, and Intl with ext-intl. Full ES2024+.
Web Platform APIs
URL, TextEncoder and TextDecoder, atob and btoa, structuredClone, performance and DOMException are built in.
fetch + Streams
Real HTTP from JS via ext-curl. Full Fetch API with AbortSignal, Blob, FormData, and WHATWG Streams.
Web Platform Tests
Imported WPT subtests for fetch, headers, blob, abort, streams and encoding pass, apart from an allowlist of brotli and SharedArrayBuffer cases.
PHP ↔ JS Interop
setGlobal() exposes PHP closures as JS functions and call() runs JS functions from PHP. Values are converted at the boundary; arrays are copied.
Embeddable
One Engine class: eval, execFile, setGlobal, call. Drop into any PHP app or CLI.
Call Stack Cap
A fixed 4,096-frame call stack stops runaway recursion with a catchable InternalError.
CLI
bin/phasis runs a file or an -e snippet and prints the AST with --ast. In a repository checkout, bin/test262 runs the conformance suite.
Node.js Oracle
Scenarios are checked against Node.js (V8). WPT and the library suite run in CI on pushes that touch the engine; the full test262 pass runs on demand, last on 2026-06-09.
Real Libraries
204 npm packages, among them acorn, mustache.js, lodash and marked, are tested with output byte-equal to Node.
Why Phasis
No Node.js process
Phasis lexes, parses and executes JavaScript in PHP itself, with no exec() call to Node and no FFI. It installs as a Composer package.
fetch under host control
fetch() uses ext-curl by default. setFetchTransport() swaps the transport, for example for a mock in tests, and setFetchPolicy() can deny or rewrite each request before it goes out.
Timers on virtual time
eval() drains the event loop before it returns, and the clock jumps to the next pending timer instead of sleeping. Long-lived hosts can drive the loop in real time with tickEventLoop().
Parser and formatter included
The parser returns a typed PHP AST that exports to ESTree. The formatter reprints JavaScript, JSON and CSS with prettier-compatible layout.
Compared with
Other ways to run or process JavaScript from a PHP application.
| Tool | Good at | Difference with Phasis |
|---|---|---|
| Node.js | Good atNode.js runs JavaScript on the V8 engine and resolves packages from node_modules folders, so npm dependencies load by name. | Difference with PhasisCalling Node from PHP adds a second runtime and a subprocess boundary. Phasis runs inside PHP, but its module loader has no node_modules or package.json exports resolution, so npm packages are bundled first, for example with esbuild. |
| V8Js | Good atV8Js embeds Google's V8 engine in PHP and runs JavaScript in a sandbox that can be restricted with a time limit and a memory limit. | Difference with PhasisV8Js is a compiled extension that needs the libv8 library. Phasis is plain PHP from Composer, and its docs put it at roughly 100 times the runtime of V8 on dispatch-bound code. |
| Peast | Good atPeast parses JavaScript into an ESTree AST on PHP 5.4 and later, and adds tree traversal, selector queries and rendering. | Difference with PhasisPhasis needs PHP 8.2. Its parser returns typed, readonly node objects with an ESTree export, and the same package can also execute the code. |
| Prettier | Good atPrettier formats JavaScript, TypeScript, JSX, CSS, HTML, Markdown, YAML and more, and installs from npm. | Difference with PhasisThe Phasis formatter covers JavaScript, JSON and CSS only. Its options mirror prettier 3.x and it runs in PHP, with no Node.js. |
Questions and answers
Short answers from the Phasis documentation.
Composer requires PHP 8.2 or later with bcmath, curl and mbstring. intl is suggested for the Intl APIs and non-ISO Temporal calendars, and openssl for AES encryption in crypto.subtle.
Phasis is a tree-walking interpreter with a bytecode VM, not a JIT. The docs put it at roughly 100 times the runtime of V8 on dispatch-bound code, a ceiling they say rarely matters for embedding work such as validation rules.
Not by name. The module loader resolves imports against the file system, needs explicit file extensions and has no node_modules or package.json exports support, so bundle a package with a tool such as esbuild and pass Phasis the bundle.
No. window, document and the DOM tree are not shipped, and neither are Node globals such as process, Buffer and require().
It is buffered on the engine rather than printed. Read it with getConsoleOutput() or getConsoleLines(), and empty the buffer with clearConsole().
An uncaught throw surfaces as Phasis\Exceptions\JsThrowable. Errors the engine raises itself, such as reading a property of null, surface as Phasis\Exceptions\TypeError, ReferenceError or RangeError, and all of them extend Phasis\Exceptions\RuntimeError. Invalid source throws Phasis\Parser\ParseError, a Phasis\Exceptions\SyntaxError, with the line, column and offset.
Start with the getting started guide, then read the API reference for the fetch hooks and the event loop.