Phasis

Getting Started

Install

composer require phasis/phasis

Phasis needs PHP 8.2 or later, plus the mbstring and bcmath extensions — both ship enabled on every mainstream PHP build (Homebrew, apt, RHEL, the official php:cli Docker image, shared-hosting providers). bcmath powers BigInt arithmetic and integer-precision number handling; without it BigInt(...) and large-integer TypedArray operations cannot run.

ext-intl is optional. Phasis works without it, but the entire Intl.* family (Collator, NumberFormat, DateTimeFormat, PluralRules, Locale, DisplayNames, ListFormat, RelativeTimeFormat, Segmenter, DurationFormat) and the non-ISO Temporal calendars (hebrew, islamic, japanese, persian, etc.) need it. Install it whenever the host application exposes JS that touches locale-aware formatting.

No exec, no FFI, no Node.js, no native extensions beyond the standard PHP build.

Run a script from the CLI

./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.js

See the CLI reference for the full command set.

Embed in PHP

<?php

require __DIR__ . '/vendor/autoload.php';

use Phasis\Engine;

$engine = new Engine();

// Evaluate an expression
$result = $engine->eval('1 + 2 * 3');
echo $result;
// 7

// Execute a script file
$engine->execFile('path/to/script.js');

// Bridge a PHP value into the JS global scope
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->eval('console.log(config.version)');
// 1.0

The Engine instance keeps its global state between calls, so subsequent eval() invocations see anything you defined earlier.

Cold-start is cheap: only the spec-load-bearing core (Object, Function, Array, Promise, Symbol, BigInt, RegExp, the error types) is installed up front. Map, TypedArray, Temporal, Intl, URL, fetch, the full Web Platform / Fetch Packs, and everything else materialize transparently on first read. Pass new Engine(eager: true) to install every built-in at construction — useful for conformance tooling that introspects descriptors before reading them; rarely needed for normal embedding.

Call JS functions from PHP

$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);
// 5

call() looks up the function by name on the global object and invokes it with the supplied PHP arguments (converted automatically to JS values).

Expose a PHP callable as a JS function

$engine->setGlobal('fetchData', function (string $url): array {
    return json_decode(file_get_contents($url), true);
});

$engine->eval('const users = fetchData("https://api.example.com/users")');

PHP closures are wrapped as JS functions. Their arguments are converted from JS to PHP on call, and their return values are converted back from PHP to JS.

Share PHP objects with JS

class Counter {
    public int $value = 0;
    public function increment(): void { $this->value++; }
}

$counter = new Counter();
$engine->setGlobal('counter', $counter);

$engine->eval('counter.increment(); counter.increment();');

echo $counter->value;
// 2

The PHP object is referenced — not copied — so mutations from JS land back in the original PHP value.

Resource limits

$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);

The loop-iteration limit is the only runtime-tunable cap. Hitting it throws an InternalError which propagates to the PHP caller as a RuntimeError you can catch. The bytecode VM has its own 4 096-frame call-stack ceiling and an 8 192-frame transpiled-closure ceiling baked in — both throw "Maximum call stack size exceeded" when crossed. See API: setLimit for the full surface.

REPL

./vendor/bin/phasis --repl
> 1 + 2
3
> const greet = name => `Hello, ${name}`
> greet('Phasis')
'Hello, Phasis'

Next steps

  • API reference — full Phasis\Engine surface.
  • Interop — value conversion rules, host functions, shared objects.
  • Compatibility — what's implemented, test262 coverage.
  • Advanced — architecture, bytecode VM, benchmarks, oracle testing.

On this page