호스트 함수
PHP 클로저나 호출 가능한 함수를 setGlobal()을 통해 엔진에 전달하면 호출 가능한 JS 함수가 됩니다. 수동 등록 단계나 스키마, 마샬링이 필요하지 않습니다. 호출 가능한 함수를 전달하기만 하면 됩니다.
기본 사항
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
JS에서 greet("world")을 실행하면:
- 문자열
"world"이 JS에서 PHP로 변환됩니다. - PHP 클로저가 변환된 인수와 함께 호출됩니다.
- 반환값이 PHP에서 JS로 다시 변환됩니다.
변환은 값 변환 규칙을 따릅니다. 인수는 복사본입니다. JS 배열이나 객체는 PHP 배열로 전달되며, 이를 PHP에서 변경해도 JS 값은 바뀌지 않습니다.
인수 개수와 매개변수
엔진은 JS 인수를 호출 가능한 함수에 위치 순서대로 그대로 전달하며 시그니처를 검사하지 않습니다. 일반 클로저는 추가 인수를 무시하고, 가변 인수 매개변수는 이를 모읍니다. 누락된 인수는 채워지지 않습니다. JS이 호출 가능한 함수에 필요한 것보다 적은 수의 인수를 전달하면 PHP이 ArgumentCountError을 던집니다. 선택적 매개변수에는 기본값을 지정하세요.
$engine->setGlobal('format', function (string $template, ...$args): string {
return vsprintf($template, $args);
});
echo $engine->eval('format("%s is %d years old", "Ada", 36)');
// Ada is 36 years old
타입 변환
인수는 JS→PHP 규칙에 따라 PHP으로 변환됩니다. 엔진은 strict_types=1을 선언한 코드에서 호출 가능한 함수를 호출하므로 PHP는 스칼라 매개변수 유형을 엄격하게 검사하며 자동 변환하지 않습니다. string 매개변수에 전달된 JS 숫자는 TypeError입니다. 호스트 함수에서 발생하는 다른 PHP 예외와 마찬가지로 호출한 쪽의 eval()에 전달되며 JS으로 전달되지는 않습니다(아래 참조).
$engine->setGlobal('len', fn(string $s): int => strlen($s));
$engine->eval('len(42)'); // PHP TypeError: Argument #1 ($s) must be of type string, int given
클로저 내부에서 직접 변환하거나((string) $value) 유니언 유형을 사용하세요(int|string).
JS으로 예외 전달
호스트 함수 내부에서 던진 일반 PHP 예외는 현재 JS 오류로 변환되지 않습니다. 모든 JS try / catch 및 .catch() 처리기를 통과해 eval() 또는 call()을 호출한 PHP 코드에 원래 클래스 그대로 전달됩니다(이슈 #20). 여기에는 \Exception, \RuntimeException, \DivisionByZeroError 및 잘못된 인수에 대해 \TypeError와 \ArgumentCountError PHP이 발생시키는 예외가 포함됩니다.
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new \DivisionByZeroError('cannot divide by zero');
}
return intdiv($a, $b);
});
try {
$engine->eval('
try { divide(10, 0); } catch (e) { /* never runs */ }
');
} catch (\DivisionByZeroError $e) {
echo $e->getMessage(); // cannot divide by zero
}
JS 코드에서 잡을 수 있는 오류를 발생시키려면 엔진 자체 오류 클래스를 던지세요. Phasis\Exceptions\TypeError, RangeError 또는 ReferenceError은 JS에서 해당 JS 오류 유형으로 전달됩니다. 그 밖의 Phasis\Exceptions\RuntimeError은 메시지와 함께 일반 Error으로 전달됩니다.
use Phasis\Exceptions\RangeError;
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new RangeError('cannot divide by zero');
}
return intdiv($a, $b);
});
$engine->eval('
try {
divide(10, 0);
} catch (e) {
console.log(e.name, e.message); // RangeError cannot divide by zero
}
');
복합 값 반환
반환값은 PHP→JS 규칙에 따라 변환됩니다. JS 객체 리터럴을 반환하려면 PHP 연관 배열을 반환하세요:
$engine->setGlobal('lookup', function (string $key): array {
return ['key' => $key, 'value' => 42, 'meta' => ['ts' => time()]];
});
$engine->eval('
const r = lookup("answer");
console.log(r.value, r.meta.ts);
');
JS 배열을 반환하려면 순차적인 PHP 목록을 반환하세요:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
PHP 객체를 반환하면 ReactPHP, Amp 또는 Guzzle의 PHP Promise를 포함해 JS undefined이 반환됩니다(값 변환 참조).
비동기 호스트 함수
호스트 함수는 동기적으로 동작합니다. PHP Promise나 다른 thenable 객체는 대기하지 않고 undefined로 변환되므로 await hostFn()은 undefined으로 해석됩니다. 작업을 동기적으로 수행하고 결과를 반환하세요. JS 코드는 호출에 await을 적용할 수 있으며, 그러면 값이 이미 해석된 Promise로 감싸집니다:
$engine->setGlobal('loadBody', function (string $url): string {
return file_get_contents($url);
});
$engine->eval('
async function main() {
const body = await loadBody("https://example.com");
console.log(body.length);
}
main();
');
HTTP의 경우 내장 fetch()이 이미 실제 JS Promise를 반환하므로 사용자 지정 fetch 전송 계층을 사용해 자체 PHP 클라이언트로 전달할 수 있습니다.
성능 참고 사항
호스트 함수 호출마다 인수와 반환값을 위해 PHP↔JS 경계를 한 번씩 통과합니다. 두 변환 모두 복사 작업이므로 O(size of value)입니다. 기본형은 일정한 시간에 처리되며 배열과 객체는 크기에 비례하는 비용이 듭니다.
반복 횟수가 많은 내부 루프에서는 작은 호출을 여러 번 하기보다 한 번의 호출로 더 많은 작업을 처리하고, 몇몇 필드만 바뀌는 경우 큰 배열을 주고받지 마세요.