<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Artur</title>
    <description>The latest articles on DEV Community by Artur (@aheinze).</description>
    <link>https://dev.to/aheinze</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810906%2F3c293e1d-5342-4198-8c08-fceca4fb8452.jpeg</url>
      <title>DEV Community: Artur</title>
      <link>https://dev.to/aheinze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aheinze"/>
    <language>en</language>
    <item>
      <title>ScriptLite — a sandboxed ECMAScript subset interpreter for PHP (with optional C extension)</title>
      <dc:creator>Artur</dc:creator>
      <pubDate>Sat, 07 Mar 2026 03:50:47 +0000</pubDate>
      <link>https://dev.to/aheinze/scriptlite-a-sandboxed-ecmascript-subset-interpreter-for-php-with-optional-c-extension-58ea</link>
      <guid>https://dev.to/aheinze/scriptlite-a-sandboxed-ecmascript-subset-interpreter-for-php-with-optional-c-extension-58ea</guid>
      <description>&lt;p&gt;I've been working on &lt;a href="https://github.com/Cockpit-HQ/Cockpit" rel="noopener noreferrer"&gt;Cockpit&lt;/a&gt;, a headless CMS, for a while now. One thing that kept coming up was the need for user-defined logic — computed fields, validation rules, content transformations, stuff like that. The kind of thing where you want your CMS users to write small snippets of logic without giving them the keys to the entire PHP runtime.&lt;/p&gt;

&lt;p&gt;I looked at existing options. V8js is heavy and a pain to deploy. Lua doesn't feel right for a web-focused CMS where most users already know JavaScript. Expression languages are too limited once you need a loop or a callback. So I started building my own.&lt;/p&gt;

&lt;p&gt;What began as a simple expression evaluator for Cockpit turned into a full ECMAScript subset interpreter: &lt;a href="https://github.com/aheinze/ScriptLite" rel="noopener noreferrer"&gt;ScriptLite&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;It runs JavaScript (ES5/ES6 subset) inside PHP. No filesystem access, no network, no &lt;code&gt;eval&lt;/code&gt;, no &lt;code&gt;require&lt;/code&gt; — 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScriptLite\Engine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// User-defined pricing rule stored in your database&lt;/span&gt;
&lt;span class="nv"&gt;$rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'
    let total = items.reduce((sum, item) =&amp;gt; sum + item.price * item.qty, 0);
    if (total &amp;gt; 100) total *= (1 - discount);
    Math.round(total * 100) / 100;
'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'qty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'qty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'discount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// $result === 98.97&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It supports the stuff people actually use day to day: arrow functions, destructuring, template literals, spread/rest, array methods (&lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;, ...), object methods, regex, try/catch, &lt;code&gt;Math&lt;/code&gt;, &lt;code&gt;JSON&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP interop
&lt;/h3&gt;

&lt;p&gt;You can pass in PHP objects directly. Scripts can read properties, call methods, and mutations flow back to your PHP side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'
    if (order.total() &amp;gt; 500) {
        order.applyDiscount(10);
        order.setStatus("vip");
    }
'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// $order-&amp;gt;status is now "vip"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pass PHP closures as callable functions, so you control exactly what capabilities the script has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'
    let users = fetchUsers();
    let active = users.filter(u =&amp;gt; u.lastLogin &amp;gt; cutoff);
    active.map(u =&amp;gt; u.email);
'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'fetchUsers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'cutoff'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-30 days'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Three execution backends
&lt;/h3&gt;

&lt;p&gt;This is the part that got a bit out of hand. I ended up building three backends:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bytecode VM&lt;/strong&gt; — compiles to bytecode, runs on a stack-based VM in pure PHP. Works everywhere, no dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PHP transpiler&lt;/strong&gt; — translates the JavaScript to PHP source code that OPcache/JIT can optimize. About 40x faster than the VM. Good for hot paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;C extension&lt;/strong&gt; — a native bytecode VM with computed-goto dispatch. About 180x faster than the PHP VM. Because at some point I thought "how fast can this actually go" and couldn't stop.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The nice thing is that the API is the same regardless of backend. The engine picks the fastest available one automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// uses C ext if loaded, else PHP VM&lt;/span&gt;
&lt;span class="nv"&gt;$engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// force pure PHP&lt;/span&gt;

&lt;span class="c1"&gt;// Same code, same results, different speed&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'items.filter(x =&amp;gt; x &amp;gt; 3)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transpiler path is interesting if you want near-native speed without a C extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Transpile once, run many times with different data&lt;/span&gt;
&lt;span class="nv"&gt;$callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTranspiledCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'config'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$callback&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$batch1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'config'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$cfg&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$callback&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$batch2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'config'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$cfg&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Possible use cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User-defined formulas&lt;/strong&gt; — let users write &lt;code&gt;price * quantity * (1 - discount)&lt;/code&gt; in a CMS, form builder, or spreadsheet-like app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation rules&lt;/strong&gt; — store rules like &lt;code&gt;value.length &amp;gt; 0 &amp;amp;&amp;amp; value.length &amp;lt;= 280&lt;/code&gt; in your database and evaluate them at runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computed fields&lt;/strong&gt; — derive a field's value from other fields using a JS expression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content transformation&lt;/strong&gt; — map, filter, reshape API payloads or database rows with user-supplied logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow / automation rules&lt;/strong&gt; — evaluate conditions and trigger actions defined by end users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flags &amp;amp; A/B rules&lt;/strong&gt; — express targeting logic as scripts instead of hardcoded PHP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional UI&lt;/strong&gt; — show/hide elements based on expressions like &lt;code&gt;status === "draft" &amp;amp;&amp;amp; role === "editor"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a standalone library with no framework dependency. &lt;code&gt;composer require aheinze/scriptlite&lt;/code&gt; and you're good.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some numbers
&lt;/h3&gt;

&lt;p&gt;Benchmarked on PHP 8.4 with 10 different workloads (fibonacci, quicksort, sieve, closures, tree traversal, matrix math, etc.):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Total time&lt;/th&gt;
&lt;th&gt;vs PHP VM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PHP VM&lt;/td&gt;
&lt;td&gt;2608 ms&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transpiler&lt;/td&gt;
&lt;td&gt;66 ms&lt;/td&gt;
&lt;td&gt;40x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C Extension&lt;/td&gt;
&lt;td&gt;14.6 ms&lt;/td&gt;
&lt;td&gt;178x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The transpiler gets within 3-4x of native PHP, which is honestly good enough for most use cases. The C extension is there for when you want to go full send.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require aheinze/scriptlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the C extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pie &lt;span class="nb"&gt;install &lt;/span&gt;aheinze/scriptlite-ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/aheinze/ScriptLite" rel="noopener noreferrer"&gt;https://github.com/aheinze/ScriptLite&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;909 tests across all backends. MIT licensed. PHP 8.3+.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>php</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
