diff --git a/.idea/php.xml b/.idea/php.xml
index 6e5004e..0c52b6c 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -12,95 +12,95 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/swytch-framework.iml b/.idea/swytch-framework.iml
index f3bf585..7642d32 100644
--- a/.idea/swytch-framework.iml
+++ b/.idea/swytch-framework.iml
@@ -5,95 +5,95 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Hooks/Api/Invoker.php b/src/Hooks/Api/Invoker.php
index 9ff6835..0b23a7f 100644
--- a/src/Hooks/Api/Invoker.php
+++ b/src/Hooks/Api/Invoker.php
@@ -87,13 +87,23 @@ public function process(ServerRequestInterface $request, ResponseInterface $resp
}
throw new InvalidRequest('Unsupported parameter type in ' . $route->class . '::' . $route->name);
}
- $component = $this->factory->make($route->class);
- $result = $componentMethod->invokeArgs($component, $arguments);
+ $result = $this->invoke($route->class, $route->name, $arguments);
if (is_string($result)) {
return $response->withBody($this->psr17Factory->createStream($result));
- } elseif ($result instanceof ResponseInterface) {
+ }
+
+ if ($result instanceof ResponseInterface) {
return $result;
}
+
return $response;
}
+
+ protected function invoke(
+ string $class,
+ string $method,
+ array $arguments
+ ): mixed {
+ return $this->factory->make($class)->$method(...$arguments);
+ }
}
diff --git a/src/Hooks/Api/SSE/Preprocessor.php b/src/Hooks/Api/SSE/Preprocessor.php
new file mode 100644
index 0000000..bbfaf29
--- /dev/null
+++ b/src/Hooks/Api/SSE/Preprocessor.php
@@ -0,0 +1,67 @@
+|null $route
+ */
+ $route = $request->getAttribute(Router::ATTRIBUTE_HANDLER);
+
+ foreach (Attributes::findTargetMethods(SseAttribute::class) as $targetMethod) {
+ if ([$targetMethod->class, $targetMethod->name] === [$route->class, $route->name]) {
+ ignore_user_abort(true);
+ set_time_limit(0);
+ ini_set('output_buffering', 'off');
+ ini_set('zlib.output_compression', false);
+ ini_set('implicit_flush', true);
+
+ // X-Accel-Buffering is only needed for Nginx, but it won't hurt on anything else.
+ $this->headers->setHeader('X-Accel-Buffering', 'no');
+ $this->headers->setHeader('Content-Type', 'text/event-stream');
+ $this->headers->setHeader('Cache-Control', 'no-cache');
+ $this->headers->setHeader('Connection', 'keep-alive');
+
+ $this->logger->debug('SSE Preprocessor: SSE route found, setting headers.');
+ $request = $request->withAttribute('sse', true);
+ $this->lifecyleHooks->removeProcessor($this->invoker, 10);
+ }
+ }
+
+ $registerComponents = new ComponentRegister($this->compiler);
+ $registerComponents->preprocess($request, $type);
+
+ return $request;
+ }
+}
diff --git a/src/Hooks/Api/SSE/Processor.php b/src/Hooks/Api/SSE/Processor.php
new file mode 100644
index 0000000..4713125
--- /dev/null
+++ b/src/Hooks/Api/SSE/Processor.php
@@ -0,0 +1,82 @@
+getAttribute('sse') ?? false) {
+ parent::process($request, $response);
+ }
+ return $response;
+ }
+
+ protected function invoke(
+ string $class,
+ string $method,
+ array $arguments
+ ): mixed {
+ register_shutdown_function(function () use ($class, $method, $arguments) {
+ // output has already been emitted... so fall back to regular output systems.
+ // send a blank line with enough data so old browser don't choke
+ echo ":" . str_repeat(" ", 8096) . "\n\n";
+
+ $component = $this->factory->make($class);
+ callback:
+ $generator = $component->$method(...$arguments);
+ if ($generator instanceof \Generator) {
+ foreach ($generator as $data) {
+ if ($data instanceof SseMessage) {
+ if (connection_aborted()) {
+ return;
+ }
+ $this->emitMessage($data);
+ }
+ }
+ }
+ if ($generator instanceof SseMessage) {
+ if (connection_aborted()) {
+ return;
+ }
+ $this->emitMessage($generator);
+ goto callback;
+ }
+ });
+
+ return null;
+ }
+
+ private function emitMessage(SseMessage $message): void
+ {
+ echo "event: " . $message->event . "\n";
+ if ($message->id) {
+ echo "id: " . $message->id . "\n";
+ }
+ if ($message->retryMs) {
+ echo "retry: " . $message->retryMs . "\n";
+ }
+ array_map(static fn($x) => print("data: {$x}\n"), explode("\n", $message->data));
+ echo "\n\n:" . str_repeat(" ", 8096) . "\n\n";
+ }
+}
diff --git a/src/LifecyleHooks.php b/src/LifecyleHooks.php
index 4d4dd0d..6eb951b 100644
--- a/src/LifecyleHooks.php
+++ b/src/LifecyleHooks.php
@@ -46,6 +46,10 @@ public function __construct(
private array $middleware = [],
private array $exceptionHandlers = [],
) {
+ }
+
+ public function load(): static
+ {
$classes = Attributes::findTargetClasses(Handler::class);
foreach ($classes as $class) {
try {
@@ -71,6 +75,8 @@ public function __construct(
continue;
}
}
+
+ return $this;
}
/**
diff --git a/src/Router/Attributes/SseRoute.php b/src/Router/Attributes/SseRoute.php
new file mode 100644
index 0000000..3aad7f5
--- /dev/null
+++ b/src/Router/Attributes/SseRoute.php
@@ -0,0 +1,16 @@
+container->get(LifecyleHooks::class);
+ $hooks->load();
/**
* @var ServerRequestCreatorInterface $requestFactory
*/
diff --git a/src/Router/SseMessage.php b/src/Router/SseMessage.php
new file mode 100644
index 0000000..1e30c0e
--- /dev/null
+++ b/src/Router/SseMessage.php
@@ -0,0 +1,14 @@
+