diff --git a/Dockerfile.swoole_coroutines b/Dockerfile.swoole_coroutines new file mode 100644 index 00000000..a02852da --- /dev/null +++ b/Dockerfile.swoole_coroutines @@ -0,0 +1,48 @@ +FROM composer:2.0 AS step0 + +ARG TESTING=true +ARG DEBUG=false + +ENV TESTING=$TESTING +ENV DEBUG=$DEBUG + +WORKDIR /usr/local/src/ + +COPY composer.* /usr/local/src/ + +RUN composer install --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM appwrite/base:0.9.0 as final + +ARG TESTING=true +ARG DEBUG=false + +ENV TESTING=$TESTING +ENV DEBUG=$DEBUG + +LABEL maintainer="team@appwrite.io" + +RUN \ + if [ "$DEBUG" == "true" ]; then \ + apk add boost boost-dev; \ + fi + +WORKDIR /usr/src/code + +COPY ./dev /usr/src/code/dev +COPY ./src /usr/src/code/src +COPY ./tests /usr/src/code/tests +COPY ./phpunit.xml /usr/src/code/phpunit.xml +COPY ./phpbench.json /usr/src/code/phpbench.json +COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor + +# Enable Extensions +RUN if [ "$DEBUG" == "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi +RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi +RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so; fi + +EXPOSE 80 + +CMD ["php", "tests/e2e/server-swoole-coroutine.php"] diff --git a/docker-compose.yml b/docker-compose.yml index 744621f0..bd2952c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: build: context: . dockerfile: Dockerfile.swoole - ports: + ports: - "9401:80" volumes: - ./dev:/usr/src/code/dev:rw @@ -25,6 +25,19 @@ services: - ./tmp/xdebug:/tmp/xdebug networks: - testing + swoole-coroutine: + build: + context: . + dockerfile: Dockerfile.swoole_coroutines + ports: + - "9402:80" + volumes: + - ./dev:/usr/src/code/dev:rw + - ./src:/usr/src/code/src + - ./tests:/usr/src/code/tests + - ./tmp/xdebug:/tmp/xdebug + networks: + - testing mariadb: image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p @@ -41,4 +54,4 @@ services: command: "mysqld --innodb-flush-method=fsync --max-connections=10000" networks: - testing: \ No newline at end of file + testing: diff --git a/src/Http/Adapter/SwooleCoroutine/Request.php b/src/Http/Adapter/SwooleCoroutine/Request.php new file mode 100644 index 00000000..1659b743 --- /dev/null +++ b/src/Http/Adapter/SwooleCoroutine/Request.php @@ -0,0 +1,364 @@ +swoole = $request; + } + + /** + * Get raw payload + * + * Method for getting the HTTP request payload as a raw string. + * + * @return string + */ + public function getRawPayload(): string + { + return $this->swoole->rawContent(); + } + + /** + * Get server + * + * Method for querying server parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string|null $default + * @return string|null + */ + public function getServer(string $key, string $default = null): ?string + { + return $this->swoole->server[$key] ?? $default; + } + + /** + * Set server + * + * Method for setting server parameters. + * + * @param string $key + * @param string $value + * @return static + */ + public function setServer(string $key, string $value): static + { + $this->swoole->server[$key] = $value; + + return $this; + } + + /** + * Get IP + * + * Returns users IP address. + * Support HTTP_X_FORWARDED_FOR header usually return + * from different proxy servers or PHP default REMOTE_ADDR + */ + public function getIP(): string + { + $ips = explode(',', $this->getHeader('x-forwarded-for', $this->getServer('remote_addr') ?? '0.0.0.0')); + + return trim($ips[0] ?? ''); + } + + /** + * Get Protocol + * + * Returns request protocol. + * Support HTTP_X_FORWARDED_PROTO header usually return + * from different proxy servers or PHP default REQUEST_SCHEME + * + * @return string + */ + public function getProtocol(): string + { + $protocol = $this->getHeader('x-forwarded-proto', $this->getServer('server_protocol') ?? 'https'); + + if ($protocol === 'HTTP/1.1') { + return 'http'; + } + + return match ($protocol) { + 'http', 'https', 'ws', 'wss' => $protocol, + default => 'https' + }; + } + + /** + * Get Port + * + * Returns request port. + * + * @return string + */ + public function getPort(): string + { + return $this->getHeader('x-forwarded-port', (string) \parse_url($this->getProtocol().'://'.$this->getHeader('x-forwarded-host', $this->getHeader('host')), PHP_URL_PORT)); + } + + /** + * Get Hostname + * + * Returns request hostname. + * + * @return string + */ + public function getHostname(): string + { + return strval(\parse_url($this->getProtocol().'://'.$this->getHeader('x-forwarded-host', $this->getHeader('host')), PHP_URL_HOST)); + } + + /** + * Get Method + * + * Return HTTP request method + * + * @return string + */ + public function getMethod(): string + { + return $this->getServer('request_method') ?? 'UNKNOWN'; + } + + /** + * Set method + * + * Set HTTP request method + * + * @param string $method + * @return static + */ + public function setMethod(string $method): static + { + $this->setServer('request_method', $method); + + return $this; + } + + /** + * Get URI + * + * Return HTTP request URI + * + * @return string + */ + public function getURI(): string + { + return $this->getServer('request_uri') ?? ''; + } + + /** + * Set URI + * + * Set HTTP request URI + * + * @param string $uri + * @return static + */ + public function setURI(string $uri): static + { + $this->setServer('request_uri', $uri); + + return $this; + } + + /** + * Get Referer + * + * Return HTTP referer header + * + * @return string + */ + public function getReferer(string $default = ''): string + { + return $this->getHeader('referer', ''); + } + + /** + * Get Origin + * + * Return HTTP origin header + * + * @return string + */ + public function getOrigin(string $default = ''): string + { + return $this->getHeader('origin', $default); + } + + /** + * Get User Agent + * + * Return HTTP user agent header + * + * @return string + */ + public function getUserAgent(string $default = ''): string + { + return $this->getHeader('user-agent', $default); + } + + /** + * Get Accept + * + * Return HTTP accept header + * + * @return string + */ + public function getAccept(string $default = ''): string + { + return $this->getHeader('accept', $default); + } + + /** + * Get files + * + * Method for querying upload files data. If $key is not found empty array will be returned. + * + * @param string $key + * @return array + */ + public function getFiles($key): array + { + $key = strtolower($key); + + return $this->swoole->files[$key] ?? []; + } + + /** + * Get cookie + * + * Method for querying HTTP cookie parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string $default + * @return string + */ + public function getCookie(string $key, string $default = ''): string + { + $key = strtolower($key); + + return $this->swoole->cookie[$key] ?? $default; + } + + /** + * Get header + * + * Method for querying HTTP header parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string $default + * @return string + */ + public function getHeader(string $key, string $default = ''): string + { + return $this->swoole->header[$key] ?? $default; + } + + /** + * Method for adding HTTP header parameters. + * + * @param string $key + * @param string $value + * @return static + */ + public function addHeader(string $key, string $value): static + { + $this->swoole->header[$key] = $value; + + return $this; + } + + /** + * Method for removing HTTP header parameters. + * + * @param string $key + * @return static + */ + public function removeHeader(string $key): static + { + if (isset($this->swoole->header[$key])) { + unset($this->swoole->header[$key]); + } + + return $this; + } + + public function getSwooleRequest(): SwooleRequest + { + return $this->swoole; + } + + /** + * Generate input + * + * Generate PHP input stream and parse it as an array in order to handle different content type of requests + * + * @return array + */ + protected function generateInput(): array + { + if (null === $this->queryString) { + $this->queryString = $this->swoole->get ?? []; + } + if (null === $this->payload) { + $contentType = $this->getHeader('content-type'); + + // Get content-type without the charset + $length = strpos($contentType, ';'); + $length = (empty($length)) ? strlen($contentType) : $length; + $contentType = substr($contentType, 0, $length); + + switch ($contentType) { + case 'application/json': + $this->payload = json_decode(strval($this->swoole->rawContent()), true); + break; + + default: + $this->payload = $this->swoole->post; + break; + } + + if (empty($this->payload)) { // Make sure we return same data type even if json payload is empty or failed + $this->payload = []; + } + } + + return match ($this->getMethod()) { + self::METHOD_POST, + self::METHOD_PUT, + self::METHOD_PATCH, + self::METHOD_DELETE => $this->payload, + default => $this->queryString + }; + } + + /** + * Generate headers + * + * Parse request headers as an array for easy querying using the getHeader method + * + * @return array + */ + protected function generateHeaders(): array + { + return $this->swoole->header; + } +} diff --git a/src/Http/Adapter/SwooleCoroutine/Response.php b/src/Http/Adapter/SwooleCoroutine/Response.php new file mode 100644 index 00000000..2c8c77a7 --- /dev/null +++ b/src/Http/Adapter/SwooleCoroutine/Response.php @@ -0,0 +1,99 @@ +swoole = $response; + parent::__construct(\microtime(true)); + } + + public function getSwooleResponse(): SwooleResponse + { + return $this->swoole; + } + + /** + * Write + * + * @param string $content + * @return bool False if write cannot complete, such as request ended by client + */ + public function write(string $content): bool + { + return $this->swoole->write($content); + } + + /** + * End + * + * @param string|null $content + * @return void + */ + public function end(string $content = null): void + { + $this->swoole->end($content); + } + + /** + * Send Status Code + * + * @param int $statusCode + * @return void + */ + protected function sendStatus(int $statusCode): void + { + $this->swoole->status((string) $statusCode); + } + + /** + * Send Header + * + * @param string $key + * @param string $value + * @return void + */ + public function sendHeader(string $key, string $value): void + { + $this->swoole->header($key, $value); + } + + /** + * Send Cookie + * + * Send a cookie + * + * @param string $name + * @param string $value + * @param array $options + * @return void + */ + protected function sendCookie(string $name, string $value, array $options): void + { + $this->swoole->cookie( + name: $name, + value: $value, + expires: $options['expire'] ?? 0, + path: $options['path'] ?? '', + domain: $options['domain'] ?? '', + secure: $options['secure'] ?? false, + httponly: $options['httponly'] ?? false, + samesite: $options['samesite'] ?? false, + ); + } +} diff --git a/src/Http/Adapter/SwooleCoroutine/Server.php b/src/Http/Adapter/SwooleCoroutine/Server.php new file mode 100644 index 00000000..d813172f --- /dev/null +++ b/src/Http/Adapter/SwooleCoroutine/Server.php @@ -0,0 +1,51 @@ +server = new SwooleServer($host, $port); + $this->server->set(\array_merge($settings, [ + 'enable_coroutine' => true + ])); + } + + public function onRequest(callable $callback) + { + $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { + $context = \strval(Coroutine::getCid()); + + Http::setResource('swooleRequest', fn () => $request, [], $context); + Http::setResource('swooleResponse', fn () => $response, [], $context); + + call_user_func($callback, new Request($request), new Response($response), $context); + }); + } + + public function onStart(callable $callback) + { + call_user_func($callback, $this); + } + + public function start() + { + if(Coroutine::getCid() === -1) { + run(fn () => $this->server->start()); + } else { + $this->server->start(); + } + } +} diff --git a/tests/e2e/server-swoole-coroutine.php b/tests/e2e/server-swoole-coroutine.php new file mode 100644 index 00000000..58995168 --- /dev/null +++ b/tests/e2e/server-swoole-coroutine.php @@ -0,0 +1,52 @@ +withHost('mariadb') + ->withPort(3306) + // ->withUnixSocket('/tmp/mysql.sock') + ->withDbName('test') + ->withCharset('utf8mb4') + ->withUsername('user') + ->withPassword('password'), 9000); + + +$dependency = new Dependency(); + +$dependency + ->setName('key') + ->inject('request') + ->setCallback(function (Request $request) { + return $request->getHeader('x-utopia-key', 'unknown'); + }); + +$container->set($dependency); + +$dependency1 = new Dependency(); +$dependency1 + ->setName('pool') + ->setCallback(function () use ($pool) { + return $pool; + }); + +$container->set($dependency1); + +$server = new Server('0.0.0.0', '80'); +$http = new Http($server, $container, 'UTC'); + +echo "Server started\n"; + +$http->start();