Saltar al contenido

Objetos compartidos

Aún no se admite pasar un objeto PHP por referencia a JavaScript —leer sus propiedades, llamar a sus métodos y ver los cambios en PHP. El tema se sigue en la incidencia n.º 18. Esta página describe el comportamiento actual y los patrones que sí funcionan.

Qué ocurre actualmente

setGlobal() convierte su valor según las reglas de Conversión de valores. Un objeto PHP que no se puede invocar no tiene conversión, así que la variable global es undefined:

PHP
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')

Lo mismo se aplica a stdClass, entidades y modelos, contenedores de servicios, generadores y recursos. Hay dos excepciones: los objetos invocables:

  • Un objeto con __invoke() se convierte en una función JS que lo llama.
  • Un array invocable, como [$counter, 'increment'], se convierte en una función JS que llama a ese método.

No existe una clase contenedora PhpBridge ni otra para exponer objetos; src/Interop/ solo contiene el conversor de valores.

Exponer métodos como cierres

Un cierre puede capturar el objeto; así, JS llama a una función y el método se ejecuta en la instancia PHP original:

PHP
$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

Un array invocable hace lo mismo sin un cierre:

PHP
$engine->setGlobal('increment', [$counter, 'increment']);

Agruparlos en un array

Un array asociativo se convierte en un objeto JS y los cierres que contiene se convierten en funciones. Así, JS obtiene un API con forma de objeto cuyos métodos actúan sobre la instancia PHP:

PHP
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

JS solo puede ver lo que incluyas en el array, así que el estado privado, como $secret, permanece en PHP.

Los campos de datos son copias tomadas cuando se ejecuta setGlobal(). En el ejemplo, user.name es una cadena en JS; asignarle otro valor cambia la copia de JS, no $user->name. Canaliza todas las escrituras mediante un cierre.

Pasar datos y devolver resultados

Para objetos con estructura de datos, conviértalos en un array al introducirlos y lea el resultado al extraerlo:

PHP
$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) o un método toArray() le proporciona el array de un objeto. Después, aplique el array devuelto a su objeto, entidad o modelo en PHP.

Advertencias

  • Las excepciones lanzadas dentro de estos cierres actualmente no pueden ser capturadas por JS try / catch; llegan al llamador de eval() (incidencia #20). Consulte Funciones del host.
  • Un cierre registrado con setGlobal() mantiene vivos los objetos que captura mientras el motor conserve el global.