Phasis
Interop

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;
// 6

The 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;
// 37

PHP 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');
// true

This 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.

On this page