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:

  1. Registration phase (evaluate):
  2. script registers handlers on the host application
  3. script usually returns null unless there is a top-level return
  4. Dispatch phase (host callback):
  5. your host calls the registered closure
  6. 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

  1. Advanced handlers must be declared inside tell application.
  2. Error code: HS4047
  3. Target application must implement ScriptHandlerHostInterface.
  4. Error code: HS4048
  5. Handler subject cannot be empty.
  6. Error code: HS4049
  7. exit repeat / next repeat are only valid inside active repeat loops.
  8. Typical codes: HS4035, HS4036, HS4037

Practical Tips

  • Normalize verb and subject in 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 after evaluate().