Skip to content

Getting Started

Install

Shell
composer require phasis/phasis

Phasis needs PHP 8.2 or later with three extensions, all required by composer.json:

  • mbstring — string handling.
  • bcmath — BigInt arithmetic and integer-precision number handling.
  • curl — the default transport behind fetch().

mbstring and curl are enabled in most builds. bcmath often is not: the official php Docker images (php:8.x-cli, php:8.x-fpm) leave it out, and some distributions package it separately (php-bcmath). Without it Composer refuses to install Phasis. In a Dockerfile based on the official image, add:

Dockerfile
RUN docker-php-ext-install bcmath

Run php -m to see which extensions your build has.

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. ext-openssl is optional as well: crypto.subtle AES-CBC, AES-CTR and AES-GCM encryption need it, while digests and HMAC work without it.

No exec, no FFI, no Node.js, no custom native extension.

Run a script from the CLI

Shell
./vendor/bin/phasis -e 'console.log(1 + 2 * 3)'
# 7

./vendor/bin/phasis -e 'console.log([1, 2, 3].map(x => x * x).join(","))'
# 1,4,9

./vendor/bin/phasis path/to/script.js

The CLI prints what the script writes with console.log; it does not print the value of the last expression.

See the CLI reference for the full command set.

Embed in PHP

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)');

// console output is buffered on the engine, not printed
echo $engine->getConsoleOutput();
// 1.0

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

console.log and its siblings append to a buffer on the engine instead of writing to stdout. Read it with getConsoleOutput() (one string, lines joined with \n) or getConsoleLines() (a list of lines), and empty it with clearConsole().

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

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

PHP
$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.

PHP objects

PHP objects cannot be shared with JavaScript yet. setGlobal() with a plain PHP object binds undefined (tracked in issue #18). Pass data as arrays, which are copied into JS, and expose behaviour as closures:

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

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

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

echo $counter->value;
// 2

See Shared objects for the details and other workarounds.

Resource limits

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

maxLoopIterations (default 100,000) is the only runtime-tunable limit, and today it only applies to code the tree-walking interpreter runs. Loops the bytecode VM runs (function bodies, and most top-level code) are not counted, so a limit alone does not stop while (true) {} (issue #19). When the limit is hit, the error is catchable in JS; uncaught, it reaches PHP as Phasis\Exceptions\InternalError, a subclass of Phasis\Exceptions\RuntimeError.

The engine also has a fixed 4,096-frame call-stack ceiling (and an 8,192-frame guard for transpiled closures); crossing it throws "Maximum call stack size exceeded", again as InternalError. There is no wall-clock limit, so run untrusted code under PHP's own max_execution_time or in a separate process. See API: setLimit.

Next steps

  • API reference — full Phasis\Engine surface.
  • Interop — value conversion rules, host functions, PHP objects.
  • Compatibility — what's implemented, test262 coverage.
  • Parser and Formatter — parse and format JavaScript without executing it.
  • Advanced — architecture, bytecode VM, benchmarks, oracle testing.