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 @@ +