Zum Inhalt springen

Erste Schritte

Installation

Shell
composer require phasis/phasis

Phasis benötigt PHP 8.2 oder höher sowie drei Erweiterungen, die alle von composer.json benötigt werden:

  • mbstring — String-Verarbeitung.
  • bcmath — BigInt-Arithmetik und präzise Verarbeitung von Ganzzahlen.
  • curl — der Standardtransport hinter fetch().

mbstring und curl sind in den meisten Builds aktiviert. bcmath ist es oft nicht: In den offiziellen php-Docker-Images (php:8.x-cli, php:8.x-fpm) fehlt es, und manche Distributionen stellen es separat bereit (php-bcmath). Ohne die Erweiterung verweigert Composer die Installation von Phasis. Füge in einem Dockerfile auf Basis des offiziellen Images Folgendes hinzu:

Dockerfile
RUN docker-php-ext-install bcmath

Führe php -m aus, um zu sehen, welche Erweiterungen dein Build enthält.

ext-intl ist optional. Phasis funktioniert auch ohne die Erweiterung, aber die gesamte Intl.*-Familie (Collator, NumberFormat, DateTimeFormat, PluralRules, Locale, DisplayNames, ListFormat, RelativeTimeFormat, Segmenter, DurationFormat) sowie die nicht-ISO-Temporal-Kalender (hebrew, islamic, japanese, persian usw.) benötigen sie. Installiere sie, wenn die Host-Anwendung JS bereitstellt, das lokalisierungsabhängige Formatierung verwendet. ext-openssl ist ebenfalls optional: Für crypto.subtle-Verschlüsselung mit AES-CBC, AES-CTR und AES-GCM wird sie benötigt, Digests und HMAC funktionieren auch ohne sie.

Kein exec, kein FFI, kein Node.js, keine benutzerdefinierte native Erweiterung.

Skript über die CLI ausführen

Shell
./vendor/bin/phasis -e 'console.log(1 + 2 * 3)'
# 7

./vendor/bin/phasis -e 'console.log([1, 2, 3].map(x => x * x).join(","))'
# 1,4,9

./vendor/bin/phasis path/to/script.js

Die CLI gibt aus, was das Skript mit console.log schreibt; den Wert des letzten Ausdrucks gibt sie nicht aus.

Siehe die CLI Referenz für den vollständigen Befehlssatz.

In PHP einbetten

PHP
<?php

require __DIR__ . '/vendor/autoload.php';

use Phasis\Engine;

$engine = new Engine();

// Evaluate an expression
$result = $engine->eval('1 + 2 * 3');
echo $result;
// 7

// Execute a script file
$engine->execFile('path/to/script.js');

// Bridge a PHP value into the JS global scope
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->eval('console.log(config.version)');

// console output is buffered on the engine, not printed
echo $engine->getConsoleOutput();
// 1.0

Die Engine-Instanz behält ihren globalen Zustand zwischen Aufrufen bei, sodass nachfolgende Aufrufe von eval() alles sehen, was du zuvor definiert hast.

console.log und seine Varianten hängen Ausgaben an einen Puffer der Engine an, statt sie nach stdout zu schreiben. Lies ihn mit getConsoleOutput() (ein String, Zeilen verbunden mit \n) oder getConsoleLines() (eine Liste von Zeilen) aus und leere ihn mit clearConsole().

Der Kaltstart ist günstig: Zunächst wird nur der Kern installiert, der die Spezifikation lädt (Object, Function, Array, Promise, Symbol, BigInt, RegExp und die Fehlertypen). Map, TypedArray, Temporal, Intl, URL, fetch, die vollständigen Web Platform- und Fetch-Packs sowie alles Weitere werden beim ersten Lesen transparent geladen. Übergib new Engine(eager: true), um bei der Erstellung alle integrierten Komponenten zu installieren – nützlich für Konformitätstools, die Deskriptoren prüfen, bevor sie sie lesen; beim normalen Einbetten selten nötig.

JS-Funktionen aus PHP aufrufen

PHP
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);
// 5

call() sucht das globale Objekt anhand des Namens nach der Funktion und ruft sie mit den übergebenen Argumenten PHP auf (automatisch in JS-Werte konvertiert).

Einen aufrufbaren Wert aus PHP als JS-Funktion bereitstellen

PHP
$engine->setGlobal('fetchData', function (string $url): array {
    return json_decode(file_get_contents($url), true);
});

$engine->eval('const users = fetchData("https://api.example.com/users")');

PHP-Closures werden als JS-Funktionen verpackt. Ihre Argumente werden beim Aufruf von JS in PHP konvertiert, und ihre Rückgabewerte werden von PHP zurück in JS konvertiert.

PHP-Objekte

PHP-Objekte können noch nicht mit JavaScript geteilt werden. setGlobal() mit einem einfachen PHP-Objekt bindet undefined (verfolgt in Issue #18). Übergib Daten als Arrays, die nach JS kopiert werden, und stelle Verhalten als Closures bereit:

PHP
class Counter {
    public int $value = 0;
    public function increment(): void { $this->value++; }
}

$counter = new Counter();
$engine->setGlobal('increment', fn () => $counter->increment());

$engine->eval('increment(); increment();');

echo $counter->value;
// 2

Weitere Details und Umgehungsmöglichkeiten findest du unter Gemeinsam genutzte Objekte.

Ressourcenlimits

PHP
$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);

maxLoopIterations (Standardwert: 100,000) ist die einzige zur Laufzeit anpassbare Grenze und gilt derzeit nur für Code, den der baumwandelnde Interpreter ausführt. Schleifen der Bytecode-VM (Funktionskörper und der Großteil des Codes auf oberster Ebene) werden nicht gezählt, daher verhindert eine Begrenzung allein while (true) {} nicht (Issue #19). Wird die Grenze erreicht, kann der Fehler in JS abgefangen werden; bleibt er unbehandelt, erreicht er PHP als Phasis\Exceptions\InternalError, einer Unterklasse von Phasis\Exceptions\RuntimeError.

Die Engine hat außerdem eine feste Obergrenze von 4,096 Frames für den Aufrufstack (und eine Schutzgrenze von 8,192 Frames für transpilierte Closures); bei Überschreitung wird erneut als InternalError der Fehler „Maximum call stack size exceeded“ ausgelöst. Es gibt keine Laufzeitbegrenzung, führe nicht vertrauenswürdigen Code daher unter PHPs eigener max_execution_time oder in einem separaten Prozess aus. Siehe API: setLimit.

Nächste Schritte