Advanced Handlers
Advanced handlers let scripts register host-dispatched callbacks inside tell application blocks.
What They Are
Syntax:
tell application "router"
on get "/"
-- body
end get
end tell
Also valid:
- on post "/create"
- on event "user.created"
Classic script handlers can still exist in the same program:
on add(a, b)
return a + b
end add
tell application "router"
on get "/sum"
return add(40, 2)
end get
end tell
Runtime Model
Advanced handlers run in two phases:
- Registration phase (
evaluate): - script registers handlers on the host application
- script usually returns
nullunless there is a top-levelreturn - Dispatch phase (host callback):
- your host calls the registered closure
- script handler body executes with dispatch event context
Host Contract
Applications that accept advanced handlers must implement:
Hyperphp\Host\ScriptHandlerHostInterface
Required method:
registerScriptHandler(string $verb, string $subject, callable $handler): void
Minimal host example:
<?php
use Hyperphp\Host\HyperApplication;
use Hyperphp\Host\ScriptHandlerHostInterface;
final class Router extends HyperApplication implements ScriptHandlerHostInterface
{
/** @var array<string, array<string, callable>> */
private array $handlers = [];
public function registerScriptHandler(string $verb, string $subject, callable $handler): void
{
$this->handlers[strtolower($verb)][strtolower($subject)] = $handler;
}
public function dispatchGet(string $path, array $eventContext = []): mixed
{
$callback = $this->handlers['get'][strtolower($path)] ?? static fn (array $_eventContext = []) => null;
return $callback($eventContext);
}
}
End-To-End Example: Register and Dispatch
Script:
tell application "router"
on get "/"
return "home"
end get
end tell
PHP:
<?php
use Hyperphp\HyperScript;
$router = new Router();
$runtime = new HyperScript();
$runtime->registerModel('router', $router);
$session = $runtime->evaluate(<<<SCRIPT
tell application "router"
on get "/"
return "home"
end get
end tell
SCRIPT);
// $session->result() is null here (registration only)
$result = $router->dispatchGet('/');
// "home"
Example: Request/Response Context
Script:
tell application "router"
on get "/"
tell response
set status to 200
set body to "hello"
end tell
return body of response
end get
end tell
PHP dispatch context:
$result = $router->dispatchGet('/', [
'request' => $request,
'response' => $response,
]);
Inside the handler, both direct keys (request, response) and the aggregate event record are available.
Example: Multiple Verbs
tell application "router"
on get "/"
return "home"
end get
on post "/create"
return name of post of request
end post
end tell
Example: Event Handler with Payload and Metadata
tell application "events"
on event "user.created"
return name of payload of event & ":" & request_id
end event
end tell
Dispatch:
$result = $events->dispatchEvent(
'user.created',
['name' => 'Ada'],
['request_id' => 7],
);
// "Ada:7"
Example: Dynamic Subject Expressions
The subject is a normal expression and is evaluated at registration time.
tell application "events"
on event "user." & "created"
return true
end event
end tell
Example: Cross-Application Orchestration
tell application "router"
on get "/publish"
tell application "events"
emit_event "page.viewed", {path: "/publish", actor: actor of request}
set emitted_result to result
end tell
tell response
set status to 202
set body to emitted_result
end tell
return body of response
end get
end tell
tell application "events"
on event "page.viewed"
tell application "database"
insert_into "events", payload of event
end tell
return "tracked:" & path of payload of event
end event
end tell
This pattern is tested in integration and is a good template for routing plus async/event workflows.
Example: Nested Command Result
result is updated after command calls and can be used inside the handler:
tell application "router"
on get "/"
tell application "twig"
render_template "index.twig", {name: "world"}
set html to result
end tell
return html
end get
end tell
Example: Handler-Local Error Recovery
tell application "events"
on event "webhook"
try
return path of payload of event
on error err
return "failed:" & err.code
end try
end event
end tell
Complete End-To-End Examples (PHP + Hyperphp Script)
Shared PHP Helpers
Use these helpers once, then reuse them across the examples below.
<?php
declare(strict_types=1);
use Hyperphp\Host\HyperApplication;
use Hyperphp\Host\HyperModel;
use Hyperphp\Host\ScriptHandlerHostInterface;
abstract class ScriptHost extends HyperApplication implements ScriptHandlerHostInterface
{
/** @var array<string, array<string, callable>> */
protected array $handlers = [];
public function registerScriptHandler(string $verb, string $subject, callable $handler): void
{
$this->handlers[strtolower($verb)][strtolower($subject)] = $handler;
}
protected function dispatch(string $verb, string $subject, array $eventContext = []): mixed
{
$callback = $this->handlers[strtolower($verb)][strtolower($subject)] ?? null;
if ($callback === null) {
throw new \RuntimeException(sprintf('No handler for %s %s', $verb, $subject));
}
return $callback($eventContext);
}
}
final class RouterHost extends ScriptHost
{
public function dispatchGet(string $path, array $eventContext = []): mixed
{
return $this->dispatch('get', $path, $eventContext);
}
}
final class EventsHost extends ScriptHost
{
/** @var list<array{name:string,payload:array<string,mixed>}> */
public array $emitted = [];
public function dispatchEvent(string $name, array $payload = [], array $extra = []): mixed
{
$context = array_merge($extra, [
'payload' => $payload,
'event_name' => $name,
]);
return $this->dispatch('event', $name, $context);
}
public function emitEvent(string $name, array $payload = []): mixed
{
$this->emitted[] = ['name' => $name, 'payload' => $payload];
return $this->dispatchEvent($name, $payload);
}
}
final class TwigHost extends HyperApplication
{
public function renderTemplate(string $template, array $data = []): string
{
return sprintf('<h1>%s:%s</h1>', $template, (string) ($data['name'] ?? ''));
}
}
final class DatabaseHost extends HyperApplication
{
/** @var list<array{table:string,data:array<string,mixed>}> */
public array $inserts = [];
public function insertInto(string $table, array $data): bool
{
$this->inserts[] = ['table' => $table, 'data' => $data];
return true;
}
}
final class RequestModel extends HyperModel
{
public function __construct(
public array $post = [],
public string $actor = 'anonymous',
) {
}
}
final class ResponseModel extends HyperModel
{
public int $status = 0;
public string $body = '';
}
Complete Example 1: Router + Template + Response Mutation
<?php
declare(strict_types=1);
use Hyperphp\HyperScript;
$router = new RouterHost();
$twig = new TwigHost();
$runtime = new HyperScript();
$runtime->registerModel('router', $router);
$runtime->registerModel('twig', $twig);
$runtime->evaluate(<<<'SCRIPT'
tell application "router"
on get "/"
tell application "twig"
render_template "index.twig", {name: "world"}
set html to result
end tell
tell response
set status to 200
set body to html
end tell
return body of response
end get
end tell
SCRIPT);
$response = new ResponseModel();
$result = $router->dispatchGet('/', [
'request' => new RequestModel(),
'response' => $response,
]);
var_dump($result); // "<h1>index.twig:world</h1>"
var_dump($response->status); // 200
var_dump($response->body); // "<h1>index.twig:world</h1>"
Complete Example 2: Router -> Events -> Database Workflow
<?php
declare(strict_types=1);
use Hyperphp\HyperScript;
$router = new RouterHost();
$events = new EventsHost();
$database = new DatabaseHost();
$runtime = new HyperScript();
$runtime->registerModel('router', $router);
$runtime->registerModel('events', $events);
$runtime->registerModel('database', $database);
$runtime->evaluate(<<<'SCRIPT'
tell application "router"
on get "/publish"
tell application "events"
emit_event "page.viewed", {path: "/publish", actor: actor of request}
set tracking_result to result
end tell
tell response
set status to 202
set body to tracking_result
end tell
return body of response
end get
end tell
tell application "events"
on event "page.viewed"
tell application "database"
insert_into "events", payload of event
end tell
return "tracked:" & path of payload of event
end event
end tell
SCRIPT);
$response = new ResponseModel();
$result = $router->dispatchGet('/publish', [
'request' => new RequestModel([], 'patrik'),
'response' => $response,
]);
var_dump($result); // "tracked:/publish"
var_dump($response->status); // 202
var_dump($response->body); // "tracked:/publish"
var_dump($events->emitted); // emitted event log
var_dump($database->inserts); // persisted event rows
Complete Example 3: Missing Context Fallback With try/on error
<?php
declare(strict_types=1);
use Hyperphp\HyperScript;
$events = new EventsHost();
$runtime = new HyperScript();
$runtime->registerModel('events', $events);
$runtime->evaluate(<<<'SCRIPT'
tell application "events"
on event "job.run"
try
return "id:" & request_id
on error err
return "fallback:" & err.code
end try
end event
end tell
SCRIPT);
$ok = $events->dispatchEvent('job.run', [], ['request_id' => 42]);
$fallback = $events->dispatchEvent('job.run');
var_dump($ok); // "id:42"
var_dump($fallback); // "fallback:HS4004"
Dispatch Context Rules
- Host context keys are available directly as references (
request_id,response,payload, ...). - The full context is also available as
event. - Context keys are normalized to lowercase snake_case (
Request-Id->request_id). - Each dispatch call gets isolated context; values do not leak across dispatches.
Constraints and Errors
- Advanced handlers must be declared inside
tell application. - Error code:
HS4047 - Target application must implement
ScriptHandlerHostInterface. - Error code:
HS4048 - Handler subject cannot be empty.
- Error code:
HS4049 exit repeat/next repeatare only valid inside activerepeatloops.- Typical codes:
HS4035,HS4036,HS4037
Practical Tips
- Normalize
verbandsubjectin your host registry for case-insensitive dispatch. - Keep route/event subjects explicit strings unless you need dynamic registration.
- Put transport objects (
request,response,payload) into dispatch context. - Use
session->getApplication('name')when you need the registered host instance afterevaluate().