Shared objects
When you call setGlobal() with a PHP object, the engine doesn't serialize it. Instead it wraps the object in a transparent proxy that forwards every JS property access and method call to the original PHP instance.
class Counter {
public int $value = 0;
public function increment(): void { $this->value++; }
public function add(int $n): void { $this->value += $n; }
}
$counter = new Counter();
$engine->setGlobal('counter', $counter);
$engine->eval('
counter.increment();
counter.add(5);
');
echo $counter->value;
// 6The PHP $counter instance and the JS counter global are the same object. There's no synchronization step.
What JS can see
- Public properties are visible on the JS object.
- Public methods are callable from JS with PHP→JS argument conversion.
- Protected and private members are invisible from JS.
- Static members are not exposed by default; you'd need to wrap the class itself.
class User {
public function __construct(
public string $name,
public int $age,
private string $secret,
) {}
public function greet(): string {
return "Hi, I'm $this->name";
}
}
$engine->setGlobal('user', new User('Ada', 36, 'topsecret'));
$engine->eval('
console.log(user.name); // "Ada"
console.log(user.age); // 36
console.log(user.greet()); // "Hi, I'm Ada"
console.log(user.secret); // undefined
');Mutating from JS
Property writes from JS land on the PHP instance:
$engine->eval('user.age = 37');
echo $user->age;
// 37PHP property type declarations are enforced — assigning a string to an int property throws a TypeError from PHP which reaches JS as a thrown Error.
Iteration
Public-property iteration works:
$engine->eval('
for (const key in user) {
console.log(key, user[key]);
}
// name Ada
// age 36
');To customize what's iterated (e.g. to expose only certain fields), implement IteratorAggregate on the PHP class.
Custom field maps with PhpBridge
When you don't want every public property exposed — or you want different names on the JS side — wrap the object in Phasis\Interop\PhpBridge:
use Phasis\Interop\PhpBridge;
$bridge = new PhpBridge($user, [
'displayName' => fn($u) => $u->name,
'greeting' => fn($u) => $u->greet(),
]);
$engine->setGlobal('user', $bridge);
$engine->eval('user.displayName + " says " + user.greeting');
// "Ada says Hi, I'm Ada"The keys of the map become JS property names; the values are PHP closures called lazily on property access.
Object identity
If the same PHP instance is referenced twice by different JS expressions, JS sees === equality:
$obj = new \stdClass();
$engine->setGlobal('a', $obj);
$engine->setGlobal('b', $obj);
$engine->eval('a === b');
// trueThis holds even after passing through host-function arguments and return values, because the engine tracks PHP-side object identity across the boundary.
Common patterns
WordPress integration — pass $wp_post directly:
$engine->setGlobal('post', $wpPost);
$engine->eval('post.post_title = "Edited from JS"');
wp_update_post($wpPost);ORM entity — pass a Doctrine / Eloquent model:
$engine->setGlobal('user', User::find($id));
$engine->eval('user.email = "new@example.com"');
$user->save();Service container — expose the whole container as a single object:
$engine->setGlobal('app', $container);
$engine->eval('app.cache.set("key", "value")');Caveats
- The engine holds a strong reference to every shared object until
reset()or engine destruction. Don't share long-lived objects unnecessarily if you have memory pressure. - Cycles between PHP and JS (PHP object containing a JS function that captures the PHP object) are not collected until the engine is destroyed.
- Magic methods (
__get,__set,__call) are honoured — JS reads/writes/calls fall through to the magic implementations transparently.