ScriptLite — a sandboxed ECMAScript subset interpreter for PHP (with optional C extension)
Source: Dev.to
What it does
It runs JavaScript (ES5/ES6 subset) inside PHP. No filesystem access, no network, no eval, no require — scripts can only touch the data you explicitly pass in. Think of it as a sandbox where users write logic and you control exactly what they can see and do.
$engine = new ScriptLite\Engine();
// User‑defined pricing rule stored in your database
$rule = '
let total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
if (total > 100) total *= (1 - discount);
Math.round(total * 100) / 100;
';
$result = $engine->eval($rule, [
"items" => [
["price" => 29.99, "qty" => 2],
["price" => 49.99, "qty" => 1],
],
"discount" => 0.1,
]);
// $result === 98.97
It supports the stuff people actually use day to day: arrow functions, destructuring, template literals, spread/rest, array methods (map, filter, reduce, …), object methods, regex, try/catch, Math, JSON, Date, and more.
PHP interop
You can pass PHP objects directly. Scripts can read properties, call methods, and mutations flow back to your PHP side:
$order = new Order(id: 42, status: 'pending');
$engine->eval('
if (order.total() > 500) {
order.applyDiscount(10);
order.setStatus("vip");
}
', ["order" => $order]);
// $order->status is now "vip"
You can also pass PHP closures as callable functions, so you control exactly what capabilities the script has:
$engine->eval('
let users = fetchUsers();
let active = users.filter(u => u.lastLogin > cutoff);
active.map(u => u.email);
', [
"fetchUsers" => fn() => $userRepository->findAll(),
"cutoff" => strtotime("-30 days"),
]);
Three execution backends
Bytecode VM
Compiles to bytecode, runs on a stack‑based VM in pure PHP. Works everywhere, no dependencies.
PHP transpiler
Translates the JavaScript to PHP source code that OPcache/JIT can optimize. About 40× faster than the VM. Good for hot paths.
C extension
A native bytecode VM with computed‑goto dispatch. About 180× faster than the PHP VM. Built out of curiosity about the ultimate speed.
The API is the same regardless of backend. The engine picks the fastest available one automatically:
$engine = new Engine(); // uses C ext if loaded, else PHP VM
$engine = new Engine(false); // force pure PHP
// Same code, same results, different speed
$result = $engine->eval('items.filter(x => x > 3)', ["items" => [1, 2, 3, 4, 5]]);
The transpiler path is interesting if you want near‑native speed without a C extension:
// Transpile once, run many times with different data
$callback = $engine->getTranspiledCallback($script, ["data", "config"]);
$result1 = $callback(["data" => $batch1, "config" => $cfg]);
$result2 = $callback(["data" => $batch2, "config" => $cfg]);
Possible use cases
- User‑defined formulas — let users write
price * quantity * (1 - discount)in a CMS, form builder, or spreadsheet‑like app. - Validation rules — store rules like
value.length > 0 && value.length.
909 tests across all backends. MIT licensed. PHP 8.3+.
Would love to hear what you think, especially if you’ve run into similar “I need users to write logic but not PHP” situations. What did you end up doing?