Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpexecjs Laravel Package

nacmartin/phpexecjs

Execute JavaScript from PHP with nacmartin/phpexecjs. This lightweight package bridges PHP and Node.js to evaluate JS snippets, pass data between languages, and add JavaScript execution to Laravel apps through a simple API.

View on GitHub
Deep Wiki
Context7

Getting Started

nacmartin/phpexecjs is a small PHP port of the Ruby ExecJS gem: you hand it a string of JavaScript, it shells out to an installed JS runtime (Node.js, PhantomJS), and returns whatever the script printed to stdout. It is deliberately thin — think "subprocess wrapper with a friendly API", not a sandbox or an embedded engine.

Minimal setup:

composer require nacmartin/phpexecjs
node -v          # or: phantomjs -v
which node       # confirm the binary PHP will see

First use case — evaluate an expression or call a function you control:

use Nacmartin\PhpExecJs\PhpExecJs;

$js = new PhpExecJs();
echo $js->evalJs("'hello' + ' ' + 'world'");   // hello world

If you need to pin a runtime or a binary, use the factory and pass options (check src/.../Runtime in the repo for the shipped runtime names and their option keys):

$js = PhpExecJs::create('NodeJs', ['node_bin' => '/usr/local/bin/node']);

Where to look first:

  1. The package's Runtime classes — these show exactly how the child process is invoked, which is where 90% of your debugging will happen.
  2. The single public entry point (evalJs) — the surface area is tiny, so read it end-to-end before building abstractions on top.
  3. Your runtime binary in the same environment PHP runs in (FPM, queue worker, container) — not just your terminal.

Good first use case: calling a self-contained JS helper (a slugifier, a template renderer, a validation routine) that already exists on the frontend, so you don't maintain a second implementation in PHP.

Implementation Patterns

Expression and function evaluation. evalJs returns a string; cast explicitly and prefer returning JSON from JS over relying on JS toString:

$result = $js->evalJs("JSON.stringify({ok: true, count: 3})");
$data = json_decode($result, true);

Context injection via JSON, not string building. Seed globals from PHP data with json_encode, which handles escaping correctly:

$context = ['user' => ['id' => 1, 'name' => 'Ada']];
$prelude = 'var context = ' . json_encode($context) . ';';
$name = $js->evalJs($prelude . 'context.user.name.toUpperCase()');

Bundling a library once, calling it many times. Read the bundle from disk once, cache the source string, and append the call each time:

$bundle = Cache::rememberForever('ssr-bundle', fn () =>
    file_get_contents(resource_path('js/ssr.js'))
);

$html = $js->evalJs(
    $bundle . "\nrender(" . json_encode($props) . ");"
);

Server-side rendering. Build a server bundle with Vite/esbuild (ssr: true), boot it with evalJs, and cache rendered HTML keyed by a hash of the props. This is the package's most common real-world use: one render call per request, not one per component.

Laravel integration. Bind a singleton in a service provider so controllers never touch the package directly, and keep the binary path in config:

// config/execjs.php
return [
    'runtime'   => 'NodeJs',
    'node_bin'  => env('EXECJS_NODE_BIN', '/usr/bin/node'),
    'timeout'   => 5,
];

// AppServiceProvider::register()
$this->app->singleton(JsRuntime::class, fn () => new JsRuntime(
    PhpExecJs::create(config('execjs.runtime'), [
        'node_bin' => config('execjs.node_bin'),
    ])
));

Wrap the package in your own JsRuntime class (evaluate + timeout + JSON decode + logging). That wrapper is where you add a timeout and stderr capture, because the package itself is minimal.

Offload heavy work. Put anything beyond a trivial eval on a queued job or a console command. Keep the synchronous request path for small, fast calls only.

Testing. Don't hit a real Node process in unit tests. Put JsRuntime behind an interface and bind a fake in TestCase::setUp. Reserve one integration test (tagged/slow) that runs against real Node to catch bundling regressions.

Custom runtimes. The package is small enough to extend; if you need Deno, Bun, or QuickJS, implement a runtime that follows the same contract as the shipped NodeJs/PhantomJs runtimes and hand it to the factory.

Gotchas and Tips

  • Every evalJs spawns a new process. Expect tens to hundreds of milliseconds of overhead, plus Node startup. Never call it inside a loop over thousands of records — batch inside JS or cache the result.
  • Not a sandbox. Treat all JS as trusted code. Never evaluate user-supplied source; pass data in via json_encode context instead of interpolating strings into code.
  • stdout is your only channel. undefined, functions, and cyclic objects serialize poorly; always end the script with JSON.stringify(...) (or console.log a JSON string) and json_decode in PHP.
  • Failures can be quiet. Depending on runtime and version, a thrown JS error may come back as stderr noise or an empty/partial result rather than a clean exception. Wrap calls in try/catch, assert on decoded structure, and log stdout+stderr together. Deliberately break a script during setup to see exactly how failure surfaces.
  • Timeouts are on you. An infinite loop in JS will hang the PHP worker. The package's defaults are thin — wrap the call with a timeout (in your JsRuntime, or swap in a symfony/process wrapper for full control over timeout and stderr).
  • PATH mismatches in production. FPM and queue workers often have a different PATH than your shell. Set an absolute node_bin in config and deploy it via env.
  • Memory and payload size. Large context objects get passed on the command line or through temp files and can hit OS limits. For big inputs, write JSON to a temp file and require/read it inside JS (require('fs').readFileSync(...)).
  • Node memory limits. Heavy bundles may need NODE_OPTIONS=--max-old-space-size=...; set it in the process environment, not in the JS.
  • Windows quoting. Shell escaping is a recurring pain point on Windows; Linux/containers are the happy path.
  • Stale package (last release Dec 2021). Pin it in composer.lock and run a compatibility test on your PHP version before adopting. For new projects, weigh alternatives: symfony/process + Node directly (best control), the V8js extension (in-process, faster, but a bigger install), or a persistent Node sidecar for high-throughput SSR.
  • Extension point worth knowing: a custom runtime class is the cleanest way to add binaries or wrap the invocation without forking the package.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle