Shared objects
Passing a PHP object into JavaScript by reference — reading its properties, calling its methods and seeing the changes in PHP — is not supported yet. It is tracked in issue #18. This page describes what happens today and the patterns that work instead.
What happens today
setGlobal() converts its value with the rules in Value conversion. A PHP object that is not callable has no conversion, so the global is undefined:
class Counter {
public int $value = 0;
public function increment(): void { $this->value++; }
}
$counter = new Counter();
$engine->setGlobal('counter', $counter);
$engine->eval('typeof counter');
// "undefined"
$engine->eval('counter.increment()');
// throws Phasis\Exceptions\TypeError:
// Cannot read properties of undefined (reading 'increment')
The same applies to stdClass, entities and models, service containers, generators and resources. Two exceptions are callable objects:
- An object with
__invoke()becomes a JS function that calls it. - A callable array such as
[$counter, 'increment']becomes a JS function that calls that method.
There is no PhpBridge or other wrapper class for exposing objects; src/Interop/ contains only the value converter.
Expose methods as closures
A closure can capture the object, so JS calls a function and the method runs on the original PHP instance:
$counter = new Counter();
$engine->setGlobal('increment', fn () => $counter->increment());
$engine->setGlobal('count', fn () => $counter->value);
$engine->eval('increment(); increment(); count()');
// 2
echo $counter->value;
// 2
A callable array does the same without a closure:
$engine->setGlobal('increment', [$counter, 'increment']);
Group them in an array
An associative array becomes a JS object, and closures inside it become functions. That gives JS an object-shaped API whose methods act on the PHP instance:
class User {
public function __construct(
public string $name,
public int $age,
private string $secret,
) {}
public function greet(): string {
return "Hi, I'm $this->name";
}
}
$user = new User('Ada', 36, 'topsecret');
$engine->setGlobal('user', [
'name' => $user->name,
'greet' => fn () => $user->greet(),
'setAge' => function (int $age) use ($user): void { $user->age = $age; },
]);
$engine->eval('user.greet()');
// "Hi, I'm Ada"
$engine->eval('user.setAge(37)');
echo $user->age;
// 37
Only what you put in the array is visible to JS, so private state such as $secret stays in PHP.
The data fields are copies taken when setGlobal() runs. In the example, user.name is a string in JS; assigning to it changes the JS copy, not $user->name. Route every write through a closure.
Pass data out, results back
For data-shaped objects, convert to an array on the way in and read the result on the way out:
$post = ['title' => 'Draft', 'tags' => ['php']];
$engine->setGlobal('post', $post);
$engine->eval('function edit(p) { p.title = "Edited from JS"; return p; }');
$post = $engine->call('edit', $post);
echo $post['title'];
// Edited from JS
get_object_vars($object) or a toArray() method gives you the array for an object. Then apply the returned array to your object, entity or model in PHP.
Caveats
- Exceptions thrown inside these closures currently cannot be caught by JS
try/catch; they reach the caller ofeval()(issue #20). See Host functions. - A closure registered with
setGlobal()keeps the objects it captures alive as long as the engine holds the global.