diff --git a/http-core/Abstracts/EventDispatchHelper.php b/http-core/Abstracts/EventDispatchHelper.php new file mode 100644 index 00000000..6dcf615b --- /dev/null +++ b/http-core/Abstracts/EventDispatchHelper.php @@ -0,0 +1,16 @@ +HTTP 404 Not Found
Powered by Swoole', $code); + } + +} diff --git a/http-core/Abstracts/ResponseHelper.php b/http-core/Abstracts/ResponseHelper.php new file mode 100644 index 00000000..1e8bf20c --- /dev/null +++ b/http-core/Abstracts/ResponseHelper.php @@ -0,0 +1,24 @@ +__call__()->{$name}(...$arguments); + } + + + /** + * @param $name + * @return mixed + */ + public function __get($name): mixed + { + // TODO: Change the autogenerated stub + return $this->__call__()->{$name}; + } + + + /** + * @param \Swoole\Http\Request $request + * @return Request + * @throws \Exception + */ + public static function create(\Swoole\Http\Request $request): Request + { + $serverRequest = ServerRequest::createServerRequest($request); + + Context::setContext(ResponseInterface::class, new Response()); + + Context::setContext(RequestInterface::class, $serverRequest); + + return Kiri::getDi()->get(Request::class); + } + + + /** + * @return string + */ + public function getProtocolVersion(): string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param string $version + * @return Request + */ + public function withProtocolVersion($version): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($version); + } + + + /** + * @return \string[][] + */ + public function getHeaders(): array + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param string $name + * @return bool + */ + public function hasHeader($name): bool + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @param string $name + * @return string[] + */ + public function getHeader($name): array + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @param string $name + * @return string + */ + public function getHeaderLine($name): string + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @param string $name + * @param string|string[] $value + * @return Request + */ + public function withHeader($name, $value): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($name, $value); + } + + + /** + * @param string $name + * @param string|string[] $value + * @return Request + */ + public function withAddedHeader($name, $value): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($name, $value); + } + + + /** + * @param string $name + * @return Request + */ + public function withoutHeader($name): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @return StreamInterface + */ + public function getBody(): StreamInterface + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param StreamInterface $body + * @return Request + */ + public function withBody(StreamInterface $body): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($body); + } + + + /** + * @return string + */ + public function getRequestTarget(): string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param mixed $requestTarget + * @return Request + */ + public function withRequestTarget($requestTarget): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($requestTarget); + } + + + /** + * @return string + */ + public function getMethod(): string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param string $method + * @return bool + */ + public function isMethod(string $method): bool + { + return $this->__call__()->{__FUNCTION__}($method); + } + + + /** + * @param string $method + * @return Request + */ + public function withMethod($method): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($method); + } + + + /** + * @return UriInterface + */ + public function getUri(): UriInterface + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param UriInterface $uri + * @param false $preserveHost + * @return Request + */ + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface + { + return $this->__call__()->{__FUNCTION__}($uri, $preserveHost); + } + + + /** + * @param string $name + * @return UploadedFileInterface|null + */ + public function file(string $name): ?UploadedFileInterface + { + $files = $this->__call__()->getUploadedFiles(); + if (empty($files) || !isset($files[$name])) { + return null; + } + return new Uploaded($files[$name]['tmp_name'], $files[$name]['name'], $files[$name]['type'], + $files[$name]['size'], $files[$name]['error']); + } + + + /** + * @param string|null $name + * @param mixed|null $default + * @return mixed + */ + private function _getParsedBody(string|null $name = null, mixed $default = null): mixed + { + $body = $this->__call__()->getParsedBody(); + if (empty($name)) { + return $body; + } + return $body[$name] ?? $default; + } + + + /** + * @return array + */ + public function all(): array + { + return $this->_getParsedBody(); + } + + + /** + * @param string $name + * @param bool|int|string|null $default + * @return mixed + */ + public function query(string $name, bool|int|string|null $default = null): mixed + { + $files = $this->__call__()->getQueryParams(); + + return $files[$name] ?? $default; + } + + + /** + * @param string $name + * @param int|bool|array|string|null $default + * @return mixed + */ + public function post(string $name, int|bool|array|string|null $default = null): mixed + { + return $this->_getParsedBody($name, $default); + } + + + /** + * @param string $name + * @param bool $required + * @return int|null + * @throws \Exception + */ + public function int(string $name, bool $required = false): ?int + { + $int = $this->_getParsedBody($name); + if (is_null($int) && $required) { + throw new \Exception('Required param "' . $name . '"'); + } + return (int)$int; + } + + + /** + * @param string $name + * @param bool $required + * @return float|null + * @throws \Exception + */ + public function float(string $name, bool $required = false): ?float + { + $int = $this->_getParsedBody($name); + if (is_null($int) && $required) { + throw new \Exception('Required param "' . $name . '"'); + } + return (float)$int; + } + + + /** + * @param string $name + * @param bool $required + * @return string|null + * @throws \Exception + */ + public function date(string $name, bool $required = false): ?string + { + $int = $this->_getParsedBody($name); + if (is_null($int) && $required) { + throw new \Exception('Required param "' . $name . '"'); + } + return (string)$int; + } + + + /** + * @param string $name + * @param bool $required + * @return int|null + * @throws \Exception + */ + public function timestamp(string $name, bool $required = false): ?int + { + $int = $this->_getParsedBody($name); + if (is_null($int) && $required) { + throw new \Exception('Required param "' . $name . '"'); + } + return (int)$int; + } + + + /** + * @param string $name + * @param bool $required + * @return string|null + * @throws \Exception + */ + public function string(string $name, bool $required = false): ?string + { + $int = $this->_getParsedBody($name); + if (is_null($int) && $required) { + throw new \Exception('Required param "' . $name . '"'); + } + return (string)$int; + } + + + /** + * @param string $name + * @param array $default + * @return array|null + */ + public function array(string $name, array $default = []): ?array + { + $int = $this->_getParsedBody($name); + if (is_null($int)) { + return $default; + } + return $int; + } + + + /** + * @return array|null + */ + public function gets(): ?array + { + return $this->__call__()->getQueryParams(); + } + + + /** + * @param string $field + * @param string $sizeField + * @param int $max + * @return float|int + */ + public function offset(string $field = 'page', string $sizeField = 'size', int $max = 100): float|int + { + $page = $this->query($field); + $size = $this->size($sizeField, $max); + $offset = ($page - 1) * $size; + if ($offset < 0) { + $offset = 0; + } + return $offset; + } + + + /** + * @param string $field + * @param int $max + * @return int + */ + public function size(string $field = 'size', int $max = 100): int + { + $size = $this->query($field); + if ($size > $max) { + $size = $max; + } + return $size; + } + + + /** + * @param $name + * @param null $default + * @return mixed + */ + public function input($name, $default = null): mixed + { + return $this->_getParsedBody($name, $default); + } + + + /** + * @return float + */ + #[Pure] public function getStartTime(): float + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param AuthIdentity $authority + */ + public function setAuthority(AuthIdentity $authority): void + { + $this->__call__()->{__FUNCTION__}($authority); + } + + + /** + * @return int + */ + public function getClientId(): int + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowOrigin(): ?string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowHeaders(): ?string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlRequestMethod(): ?string + { + return $this->__call__()->{__FUNCTION__}(); + } +} diff --git a/http-core/Constrict/RequestInterface.php b/http-core/Constrict/RequestInterface.php new file mode 100644 index 00000000..d48d3bf9 --- /dev/null +++ b/http-core/Constrict/RequestInterface.php @@ -0,0 +1,155 @@ + ContentType::JSON, + 'charset' => 'utf-8' + ]); + $this->withContentType($contentType['format'] ?? ContentType::JSON) + ->withCharset($contentType['charset'] ?? 'utf-8'); + } + + + /** + * @param string $name + * @return mixed + */ + public function __get(string $name) + { + return $this->__call__()->{$name}; + } + + + /** + * @return Psr7Response + */ + public function __call__(): Psr7Response + { + return Context::getContext(ResponseInterface::class, new Psr7Response()); + } + + + /** + * @return string + */ + public function getProtocolVersion(): string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param string $version + * @return ResponseInterface|Psr7Response + */ + public function withProtocolVersion($version): ResponseInterface|Psr7Response + { + return $this->__call__()->{__FUNCTION__}($version); + } + + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param string $name + * @return bool + */ + public function hasHeader($name): bool + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @param string $name + * @return string + */ + public function getHeader($name): string + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @param string $name + * @return string + */ + public function getHeaderLine($name): string + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @param string $name + * @param string|string[] $value + * @return ResponseInterface|Psr7Response + */ + public function withHeader($name, $value): ResponseInterface|Psr7Response + { + return $this->__call__()->{__FUNCTION__}($name, $value); + } + + + /** + * @param string $name + * @param string|string[] $value + * @return ResponseInterface|Psr7Response + */ + public function withAddedHeader($name, $value): ResponseInterface|Psr7Response + { + return $this->__call__()->{__FUNCTION__}($name, $value); + } + + + /** + * @param string $name + * @return ResponseInterface|Psr7Response + */ + public function withoutHeader($name): ResponseInterface|Psr7Response + { + return $this->__call__()->{__FUNCTION__}($name); + } + + + /** + * @return StreamInterface + */ + public function getBody(): StreamInterface + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param StreamInterface $body + * @return ResponseInterface|Psr7Response + */ + public function withBody(StreamInterface $body): ResponseInterface|Psr7Response + { + return $this->__call__()->{__FUNCTION__}($body); + } + + /** + * @return int + */ + public function getStatusCode(): int + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param int $code + * @param string $reasonPhrase + * @return ResponseInterface|Psr7Response + */ + public function withStatus($code, $reasonPhrase = ''): ResponseInterface|Psr7Response + { + return $this->__call__()->{__FUNCTION__}($code, $reasonPhrase); + } + + + /** + * @return string + */ + public function getReasonPhrase(): string + { + return $this->__call__()->{__FUNCTION__}(); + } + + /** + * @param string $path + * @return OnDownloadInterface + */ + public function file(string $path): OnDownloadInterface + { + return $this->__call__()->{__FUNCTION__}($path); + } + + /** + * @param $responseData + * @return string|array|bool|int|null + */ + public function _toArray($responseData): string|array|null|bool|int + { + return $this->__call__()->{__FUNCTION__}($responseData); + } + + /** + * @param $data + * @return ResponseInterface + */ + public function xml($data): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($data); + } + + /** + * @param $data + * @return ResponseInterface + */ + public function html($data): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($data); + } + + /** + * @param $data + * @return ResponseInterface + */ + public function json($data): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($data); + } + + /** + * @return string + */ + public function getContentType(): string + { + return $this->__call__()->{__FUNCTION__}(); + } + + /** + * @return bool + */ + public function hasContentType(): bool + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @param string $type + * @return ResponseInterface + */ + public function withContentType(string $type): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($type); + } + + + /** + * @param string|null $value + * @return ResponseInterface + */ + public function withAccessControlAllowOrigin(?string $value): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($value); + } + + + /** + * @param string|null $value + * @return ResponseInterface + */ + public function withAccessControlRequestMethod(?string $value): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($value); + } + + + /** + * @param string|null $value + * @return ResponseInterface + */ + public function withAccessControlAllowHeaders(?string $value): ResponseInterface + { + return $this->__call__()->{__FUNCTION__}($value); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowOrigin(): ?string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowHeaders(): ?string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlRequestMethod(): ?string + { + return $this->__call__()->{__FUNCTION__}(); + } + + + /** + * @return int + */ + public function getClientId(): int + { + if (!Context::hasContext('client.id.property')) { + $request = Context::getContext(RequestInterface::class, new RequestMessage()); + return Context::setContext('client.id.property', $request->getClientId()); + } + return (int)Context::getContext('client.id.property'); + } + + + /** + * @return array + */ + public function getClientInfo(): array + { + if (!Context::hasContext('client.info.property')) { + $request = Context::getContext(RequestInterface::class, new RequestMessage()); + + $server = Kiri::getDi()->get(ServerManager::class)->getServer(); + + $clientInfo = $server->getClientInfo($request->getClientId()); + + return Context::setContext('client.info.property', $clientInfo); + } + return Context::getContext('client.info.property'); + } + +} diff --git a/http-core/Constrict/ResponseEmitter.php b/http-core/Constrict/ResponseEmitter.php new file mode 100644 index 00000000..b18015ec --- /dev/null +++ b/http-core/Constrict/ResponseEmitter.php @@ -0,0 +1,50 @@ +getHeaders())) { + foreach ($emitter->getHeaders() as $name => $values) { + $response->header($name, implode(';', $values)); + } + } + if (is_array($emitter->getCookieParams())) { + foreach ($emitter->getCookieParams() as $name => $cookie) { + $response->cookie($name, ...$cookie); + } + } + $response->setStatusCode($emitter->getStatusCode()); + $response->header('Server', 'swoole'); + $response->header('Swoole-Version', swoole_version()); + if (!($emitter instanceof OnDownloadInterface)) { + $response->end($emitter->getBody()->getContents()); + } else { + $emitter->dispatch($response); + } + } + +} diff --git a/http-core/Constrict/ResponseInterface.php b/http-core/Constrict/ResponseInterface.php new file mode 100644 index 00000000..b411af99 --- /dev/null +++ b/http-core/Constrict/ResponseInterface.php @@ -0,0 +1,105 @@ +withContentType(ContentType::HTML)->withCharset('utf-8'); + if ($exception->getCode() == 404) { + return $response->withBody(new Stream($exception->getMessage())) + ->withStatus(404); + } + $code = $exception->getCode() == 0 ? 500 : $exception->getCode(); + return $response->withBody(new Stream(jTraceEx($exception, null, true))) + ->withStatus($code); + } + +} diff --git a/http-core/Handler/Abstracts/BaseContext.php b/http-core/Handler/Abstracts/BaseContext.php new file mode 100644 index 00000000..78f00ef6 --- /dev/null +++ b/http-core/Handler/Abstracts/BaseContext.php @@ -0,0 +1,13 @@ +middlewares) || !isset($this->middlewares[$this->offset])) { + return $this->dispatcher($request); + } + + $middleware = $this->middlewares[$this->offset]; + if (!($middleware instanceof MiddlewareInterface)) { + throw new Exception('get_implements_class($middleware) not found method process.'); + } + + $this->offset++; + + return $middleware->process($request, $this); + } + + + /** + * @param ServerRequestInterface $request + * @return mixed + * @throws Exception + */ + public function dispatcher(ServerRequestInterface $request): mixed + { + $response = call_user_func($this->handler->callback, ...$this->handler->params); + if (!($response instanceof ResponseInterface)) { + $response = $this->transferToResponse($response); + } + $response->withHeader('Run-Time', $this->_runTime($request)); + return $response; + } + + + /** + * @param ServerRequest $request + * @return float + */ + private function _runTime(ServerRequestInterface $request): float + { + $float = microtime(true) - time(); + + $serverParams = $request->getServerParams(); + + $rTime = $serverParams['request_time_float'] - $serverParams['request_time']; + + return round($float - $rTime, 6); + } + + + /** + * @param mixed $responseData + * @return \Server\Constrict\ResponseInterface + * @throws Exception + */ + private function transferToResponse(mixed $responseData): ResponseInterface + { + $interface = response()->withStatus(200); + if (!$interface->hasContentType()) { + $interface->withContentType('application/json;charset=utf-8'); + } + if (str_contains($interface->getContentType(), 'xml')) { + if (is_object($responseData)) { + $responseData = get_object_vars($responseData); + } + $interface->getBody()->write(Help::toXml($responseData)); + } else if (is_array($responseData)) { + $interface->getBody()->write(json_encode($responseData)); + } else { + $interface->getBody()->write((string)$responseData); + } + return $interface; + } + + +} diff --git a/http-core/Handler/Abstracts/HandlerManager.php b/http-core/Handler/Abstracts/HandlerManager.php new file mode 100644 index 00000000..742cedee --- /dev/null +++ b/http-core/Handler/Abstracts/HandlerManager.php @@ -0,0 +1,70 @@ + $handlers) { + $array[] = [ + 'path' => $path, + 'method' => implode(',', array_keys($handlers)) + ]; + } + return $array; + } + +} diff --git a/http-core/Handler/Abstracts/HttpService.php b/http-core/Handler/Abstracts/HttpService.php new file mode 100644 index 00000000..dc026ae4 --- /dev/null +++ b/http-core/Handler/Abstracts/HttpService.php @@ -0,0 +1,52 @@ +getLogger(); + $logger->write($message, $category); + } + + /** + * @param $name + * @return mixed + * @throws Exception + */ + public function __get($name): mixed + { + if (method_exists($this, $name)) { + return $this->{$name}(); + } + $handler = 'get' . ucfirst($name); + if (method_exists($this, $handler)) { + return $this->{$handler}(); + } + if (property_exists($this, $name)) { + return $this->$name; + } + $message = sprintf('method %s::%s not exists.', static::class, $name); + throw new Exception($message); + } + +} diff --git a/http-core/Handler/Abstracts/Middleware.php b/http-core/Handler/Abstracts/Middleware.php new file mode 100644 index 00000000..a533aa9b --- /dev/null +++ b/http-core/Handler/Abstracts/Middleware.php @@ -0,0 +1,20 @@ + + */ + private static array $_middlewares = []; + + + /** + * @param $class + * @param $method + * @param array|string|null $middlewares + * @return bool + */ + public static function add($class, $method, array|string|null $middlewares): bool + { + [$class, $method] = static::setDefault($class, $method); + if (empty($middlewares)) { + return false; + } + if (is_string($middlewares)) { + $middlewares = [$middlewares]; + } + $source = &static::$_middlewares[$class][$method]; + foreach ($middlewares as $middleware) { + $middleware = di($middleware); + if (in_array($middleware, $source)) { + continue; + } + $source[] = $middleware; + } + return true; + } + + + /** + * @param $class + * @param $method + * @return array + */ + private static function setDefault($class, $method): array + { + if (is_object($class)) { + $class = $class::class; + } + if (!isset(static::$_middlewares[$class])) { + static::$_middlewares[$class] = []; + } + if (!isset(static::$_middlewares[$class][$method])) { + static::$_middlewares[$class][$method] = []; + } + return [$class, $method]; + } + + + /** + * @param $handler + * @return Iterator|null + */ + public static function get($handler): ?array + { + if (!($handler instanceof Closure)) { + return static::$_middlewares[$handler[0]][$handler[1]] ?? null; + } + return null; + } + + +} diff --git a/http-core/Handler/Annotation/ControllerTarget.php b/http-core/Handler/Annotation/ControllerTarget.php new file mode 100644 index 00000000..8fe601af --- /dev/null +++ b/http-core/Handler/Annotation/ControllerTarget.php @@ -0,0 +1,9 @@ +getAccessControlRequestMethod(); + $allowHeaders = $request->getAccessControlAllowHeaders(); + + if (empty($requestMethod)) $requestMethod = '*'; + if (empty($allowHeaders)) $allowHeaders = '*'; + + $this->response->withAccessControlAllowOrigin('*')->withAccessControlRequestMethod($requestMethod) + ->withAccessControlAllowHeaders($allowHeaders); + + return $handler->handle($request); + } + +} diff --git a/http-core/Handler/DataGrip.php b/http-core/Handler/DataGrip.php new file mode 100644 index 00000000..9d3aaf66 --- /dev/null +++ b/http-core/Handler/DataGrip.php @@ -0,0 +1,10 @@ +execute($request); + } +} diff --git a/http-core/Handler/Formatter/FileFormatter.php b/http-core/Handler/Formatter/FileFormatter.php new file mode 100644 index 00000000..b7a59678 --- /dev/null +++ b/http-core/Handler/Formatter/FileFormatter.php @@ -0,0 +1,50 @@ +data = $context; + return $this; + } + + /** + * @return mixed + */ + public function getData(): mixed + { + $data = $this->data; + $this->clear(); + return $data; + } + + + public function clear(): void + { + $this->data = null; + unset($this->data); + } +} diff --git a/http-core/Handler/Formatter/HtmlFormatter.php b/http-core/Handler/Formatter/HtmlFormatter.php new file mode 100644 index 00000000..cf7bc317 --- /dev/null +++ b/http-core/Handler/Formatter/HtmlFormatter.php @@ -0,0 +1,61 @@ +data = $context; + return $this; + } + + /** + * @return mixed + */ + public function getData(): mixed + { + $data = $this->data; + $this->clear(); + return $data; + } + + public function clear(): void + { + $this->data = null; + unset($this->data); + } +} diff --git a/http-core/Handler/Formatter/IFormatter.php b/http-core/Handler/Formatter/IFormatter.php new file mode 100644 index 00000000..9d3a2702 --- /dev/null +++ b/http-core/Handler/Formatter/IFormatter.php @@ -0,0 +1,33 @@ +data = $context; + return $this; + } + + /** + * @return mixed + */ + public function getData(): mixed + { + $data = $this->data; + $this->clear(); + return $data; + } + + + public function clear(): void + { + $this->data = null; + unset($this->data); + } +} diff --git a/http-core/Handler/Formatter/XmlFormatter.php b/http-core/Handler/Formatter/XmlFormatter.php new file mode 100644 index 00000000..b010cc00 --- /dev/null +++ b/http-core/Handler/Formatter/XmlFormatter.php @@ -0,0 +1,89 @@ +'); + + $this->toXml($dom, $context); + + $this->data = $dom->saveXML(); + } + return $this; + } + + /** + * @return string|null + */ + public function getData(): ?string + { + $data = $this->data; + $this->clear(); + return $data; + } + + /** + * @param SimpleXMLElement $dom + * @param $data + */ + public function toXml(SimpleXMLElement $dom, $data) + { + foreach ($data as $key => $val) { + if (is_numeric($key)) { + $key = 'item' . $key; + } + if (is_array($val)) { + $node = $dom->addChild($key); + $this->toXml($node, $val); + } else if (is_object($val)) { + $val = get_object_vars($val); + $node = $dom->addChild($key); + $this->toXml($node, $val); + } else { + $dom->addChild($key, htmlspecialchars((string)$val)); + } + } + } + + public function clear(): void + { + $this->data = null; + unset($this->data); + } +} diff --git a/http-core/Handler/Handler.php b/http-core/Handler/Handler.php new file mode 100644 index 00000000..fdc6f67a --- /dev/null +++ b/http-core/Handler/Handler.php @@ -0,0 +1,94 @@ +route = $route; + + $this->_injectParams($callback); + + $this->callback = $callback; + + $dispatcher = Kiri::getDi()->get(EventProvider::class); + $dispatcher->on(OnAfterWorkerStart::class, function () { + if ($this->callback instanceof Closure) { + return; + } + $this->_middlewares = MiddlewareManager::get($this->callback); + + $aspect = NoteManager::getSpecify_annotation(Aspect::class, $this->callback[0], $this->callback[1]); + + $this->callback[0] = Kiri::getDi()->get($this->callback[0]); + if (!is_null($aspect)) { + $this->recover($aspect); + } + }); + } + + + /** + * @param Aspect $aspect + */ + public function recover(Aspect $aspect) + { + $aspect = Kiri::getDi()->get($aspect->aspect); + if (empty($aspect)) { + return; + } + $callback = $this->callback; + $params = $this->params; + + $this->params = []; + $this->callback = static function () use ($aspect, $callback, $params) { + $aspect->before(); + $result = $aspect->invoke($callback, $params); + $aspect->after($result); + return $result; + }; + } + + + /** + * @param array|Closure $callback + * @throws \ReflectionException + */ + private function _injectParams(array|Closure $callback) + { + $container = Kiri::getDi(); + if (!($callback instanceof Closure)) { + $this->params = $container->getMethodParameters($callback[0], $callback[1]); + } else { + $this->params = $container->getFunctionParameters($callback); + } + } +} diff --git a/http-core/Handler/Pipeline.php b/http-core/Handler/Pipeline.php new file mode 100644 index 00000000..7a74c389 --- /dev/null +++ b/http-core/Handler/Pipeline.php @@ -0,0 +1,179 @@ +passable = $passable; + return $this; + } + + + /** + * @param $middle + * @return $this + */ + public function overall($middle): static + { + $this->overall = $middle; + return $this; + } + + + /** + * 调用栈 + * @param $pipes + * @return $this + */ + public function through($pipes): static + { + if (empty($pipes)) return $this; + if (empty($this->pipes)) { + $this->pipes = is_array($pipes) ? $pipes : func_get_args(); + } else { + foreach ($pipes as $pipe) { + $this->pipes[] = $pipe; + } + } + return $this; + } + + /** + * 执行 + * @param callable $destination + * @return static + */ + public function then(callable $destination): static + { + $parameters = $this->passable; + if (!empty($this->overall)) { + array_unshift($this->pipes, $this->overall); + } + if (is_array($destination)) { + $destination = $this->aspect_caller($destination, $parameters); + } + $this->pipeline = array_reduce(array_reverse($this->pipes), $this->carry(), + static function () use ($destination, $parameters) { + return call_user_func($destination, ...$parameters); + } + ); + return $this->clear(); + } + + + /** + * @return $this + */ + private function clear(): static + { + $this->pipes = []; + $this->passable = null; + $this->overall = null; + return $this; + } + + + /** + * @param $destination + * @param $parameters + * @return Closure|array + */ + private function aspect_caller($destination, $parameters): Closure|array + { + [$controller, $action] = $destination; + /** @var Aspect $aop */ + $aop = NoteManager::getSpecify_annotation(Aspect::class, $controller::class, $action); + if (!empty($aop)) { + $aop = Kiri::getDi()->get($aop->aspect); + $destination = static function () use ($aop, $destination, $parameters) { + /** @var IAspect $aop */ + $aop->before(); + $aop->after($data = $aop->invoke($destination, $parameters)); + return $data; + }; + } + return $destination; + } + + + /** + * @param $request + * @return mixed + */ + public function interpreter($request): mixed + { + return call_user_func($this->pipeline, $request); + } + + + /** + * 设置异常处理器 + * @param callable $handler + * @return $this + */ + public function whenException(callable $handler): static + { + $this->exceptionHandler = $handler; + return $this; + } + + + /** + * @return Closure + */ + protected function carry(): Closure + { + return static function ($stack, $pipe) { + return static function ($passable) use ($stack, $pipe) { + if ($pipe instanceof MiddlewareInterface) { + $pipe = [$pipe, 'process']; + } + return $pipe($passable, $stack); + }; + }; + } + + /** + * 异常处理 + * @param $passable + * @param Throwable $e + * @return mixed + * @throws Throwable + */ + protected function handleException($passable, Throwable $e): mixed + { + if ($this->exceptionHandler) { + return call_user_func($this->exceptionHandler, $passable, $e); + } + throw $e; + } + +} diff --git a/http-core/Handler/Router.php b/http-core/Handler/Router.php new file mode 100644 index 00000000..15682e4b --- /dev/null +++ b/http-core/Handler/Router.php @@ -0,0 +1,263 @@ +get(Router::class); + $router->addRoute('SOCKET', $route, $handler); + } + + + /** + * @param $route + * @param $handler + * @return void + * @throws + */ + public static function post($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + $router->addRoute('POST', $route, $handler); + } + + /** + * @param $route + * @param $handler + * @return void + * @throws + */ + public static function get($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + $router->addRoute('GET', $route, $handler); + } + + + /** + * @param $route + * @param $handler + * @return void + * @throws + */ + public static function options($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + $router->addRoute('OPTIONS', $route, $handler); + } + + + /** + * @param $route + * @param $handler + * @throws + */ + public static function any($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + foreach ($router->methods as $method) { + $router->addRoute($method, $route, $handler); + } + } + + /** + * @param $route + * @param $handler + * @return void + * @throws + */ + public static function delete($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + $router->addRoute('DELETE', $route, $handler); + } + + + /** + * @param $route + * @param $handler + * @return void + * @throws Exception + */ + public static function head($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + $router->addRoute('HEAD', $route, $handler); + } + + + /** + * @param $route + * @param $handler + * @return void + * @throws + */ + public static function put($route, $handler): void + { + $router = Kiri::getDi()->get(Router::class); + $router->addRoute('PUT', $route, $handler); + } + + + /** + * @param string|array $method + * @param string $route + * @param string|Closure $closure + * @throws \ReflectionException + */ + public function addRoute(string|array $method, string $route, string|Closure $closure) + { + if (!is_array($method)) $method = [$method]; + $route = $this->getPath($route); + if (is_string($closure)) { + $closure = explode('@', $closure); + $closure[0] = $this->addNamespace($closure[0]); + if (!class_exists($closure[0])) { + return; + } + $this->addMiddlewares(...$closure); + } + foreach ($method as $value) { + HandlerManager::add($route, $value, new Handler($route, $closure)); + } + } + + + /** + * @param array $config + * @param Closure $closure + */ + public static function group(array $config, Closure $closure) + { + $router = Kiri::getDi()->get(Router::class); + + array_push($router->groupTack, $config); + + call_user_func($closure, $router); + + array_pop($router->groupTack); + } + + + /** + * @param string $route + * @return string + */ + protected function getPath(string $route): string + { + $route = ltrim($route, '/'); + $prefix = array_column($this->groupTack, 'prefix'); + if (empty($prefix = array_filter($prefix))) { + return '/' . $route; + } + return '/' . implode('/', $prefix) . '/' . $route; + } + + + /** + * @param $controller + * @param $method + */ + protected function addMiddlewares($controller, $method) + { + $middleware = array_column($this->groupTack, 'middleware'); + if (empty($middleware = array_filter($middleware))) { + return; + } + foreach ($middleware as $value) { + MiddlewareManager::add($controller, $method, $value); + } + } + + + /** + * @param $class + * @return string|null + */ + protected function addNamespace($class): ?string + { + $middleware = array_column($this->groupTack, 'namespace'); + if (empty($middleware = array_filter($middleware))) { + return $class; + } + $middleware[] = $class; + return implode('\\', array_map(function ($value) { + return trim($value, '\\'); + }, $middleware)); + } + + + /** + * @throws Exception + */ + public function read_files() + { + $this->loadRouteDir(APP_PATH . 'routes'); + } + + + /** + * @param $path + * @throws Exception + * 加载目录下的路由文件 + */ + private function loadRouteDir($path) + { + $files = glob($path . '/*'); + for ($i = 0; $i < count($files); $i++) { + if (is_dir($files[$i])) { + $this->loadRouteDir($files[$i]); + } else { + $this->loadRouterFile($files[$i]); + } + } + } + + + /** + * @param $files + * @throws Exception + */ + private function loadRouterFile($files) + { + try { + include_once "Router.php"; + } catch (Throwable $exception) { + di(Logger::class)->error('router', [ + $exception->getMessage(), + $exception->getFile(), + $exception->getLine(), + ]); + } finally { + if (isset($exception)) { + unset($exception); + } + } + } + + +} diff --git a/http-core/Http.php b/http-core/Http.php new file mode 100644 index 00000000..7b8c6d78 --- /dev/null +++ b/http-core/Http.php @@ -0,0 +1,123 @@ +exceptionHandler = Kiri::getDi()->get($exceptionHandler); + $this->responseEmitter = Kiri::getDi()->get(ResponseEmitter::class); + } + + + /** + * @param Request $request + * @param Response $response + * @throws Exception + */ + public function onRequest(Request $request, Response $response): void + { + try { + [$PsrRequest, $PsrResponse] = $this->initRequestResponse($request); + /** @var Handler $handler */ + $handler = HandlerManager::get($request->server['request_uri'], $request->getMethod()); + if (is_integer($handler)) { + $PsrResponse->withStatus($handler)->withBody(new Stream('Allow Method[' . $request->getMethod() . '].')); + } else if (is_null($handler)) { + $PsrResponse->withStatus(404)->withBody(new Stream('Page not found.')); + } else { + $PsrResponse = $this->handler($handler, $PsrRequest); + } + } catch (\Throwable $throwable) { + $PsrResponse = $this->exceptionHandler->emit($throwable, $this->response); + } finally { + $this->responseEmitter->sender($response, $PsrResponse); + $this->eventDispatch->dispatch(new OnAfterRequest()); + } + } + + + /** + * @param Handler $handler + * @param $PsrRequest + * @return ResponseInterface + * @throws Exception + */ + protected function handler(Handler $handler, $PsrRequest): \Psr\Http\Message\ResponseInterface + { + $dispatcher = new Dispatcher($handler, $handler->_middlewares); + return $dispatcher->handle($PsrRequest); + } + + + /** + * @param Request $request + * @return array + * @throws Exception + */ + private function initRequestResponse(Request $request): array + { + $PsrResponse = Context::setContext(ResponseInterface::class, new \Http\Message\Response()); + + $PsrRequest = Context::setContext(RequestInterface::class, ServerRequest::createServerRequest($request)); + if ($PsrRequest->isMethod('OPTIONS')) { + $request->server['request_uri'] = '/*'; + } + return [$PsrRequest, $PsrResponse]; + } + + +} diff --git a/http-core/Message/ContentType.php b/http-core/Message/ContentType.php new file mode 100644 index 00000000..a3a3ce23 --- /dev/null +++ b/http-core/Message/ContentType.php @@ -0,0 +1,372 @@ +version; + } + + + /** + * @param $version + * @return static + */ + public function withProtocolVersion($version): static + { + $this->version = $version; + return $this; + } + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + + /** + * @param $name + * @return bool + */ + public function hasHeader($name): bool + { + return array_key_exists($name, $this->headers); + } + + + /** + * @param $name + * @return string|array|null + */ + #[Pure] public function getHeader($name): string|null|array + { + if (!$this->hasHeader($name)) { + return null; + } + return $this->headers[$name]; + } + + + /** + * @return array + */ + public function parse_curl_header(): array + { + $_headers = []; + foreach ($this->headers as $key => $val) { + $_headers[] = $key . ': ' . implode(';', $val); + } + return $_headers; + } + + + /** + * @throws \Exception + */ + public function withData(string $headerString): static + { + [$headers, $body] = explode("\r\n\r\n", $headerString); + + $this->stream = new Stream($body); + + return $this->slip_headers($headers); + } + + + /** + * @param $headers + * @return $this + * @throws \Exception + */ + private function slip_headers($headers): static + { + $headers = explode("\r\n", $headers); + + $this->resolve_status(array_shift($headers)); + + foreach ($headers as $header) { + [$key, $value] = explode(': ', $header); + $this->withHeader($key, $value); + } + return $this; + } + + + /** + * @param string $protocol + */ + private function resolve_status(string $protocol) + { + if ($this instanceof ResponseInterface) { + [$sch, $status, $message] = explode(' ', $protocol); + [$sch, $protocolVersion] = explode('/', $sch); + $this->withProtocolVersion($protocolVersion) + ->withStatus(intval($status)); + } + } + + + /** + * @param $key + * @param $value + */ + private function addRequestHeader($key, $value) + { + $this->headers[$key] = [$value]; + } + + + /** + * @param $name + * @return string|null + */ + #[Pure] public function getHeaderLine($name): string|null + { + if ($this->hasHeader($name)) { + return implode(';', $this->headers[$name]); + } + return null; + } + + + /** + * @return string|null + */ + #[Pure] public function getContentType(): ?string + { + return $this->getHeaderLine('Content-Type'); + } + + + /** + * @param $name + * @param $value + * @return static + */ + public function withHeader($name, $value): static + { + if (!is_array($value)) { + $value = [$value]; + } + $this->headers[$name] = $value; + return $this; + } + + + /** + * @param array $headers + * @return static + */ + public function withHeaders(array $headers): static + { + $this->headers = $headers; + return $this; + } + + + /** + * @param $name + * @param $value + * @return static + * @throws + */ + public function withAddedHeader($name, $value): static + { + if (!array_key_exists($name, $this->headers)) { + throw new \Exception('Headers `' . $name . '` not exists.'); + } + $this->headers[$name][] = $value; + return $this; + } + + + /** + * @param $name + * @return static + */ + public function withoutHeader($name): static + { + unset($this->headers[$name]); + return $this; + } + + + /** + * @return null|array + */ + public function getCookieParams(): ?array + { + return $this->cookieParams; + } + + + /** + * @param array|null $cookies + * @return static + */ + public function withCookieParams(?array $cookies): static + { + $this->cookieParams = $cookies; + return $this; + } + + /** + * @return StreamInterface + */ + public function getBody(): StreamInterface + { + return $this->stream; + } + + + /** + * @param StreamInterface $body + * @return static + */ + public function withBody(StreamInterface $body): static + { + $this->stream = $body; + return $this; + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowOrigin(): ?string + { + return $this->getHeaderLine('Access-Control-Allow-Origin'); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowHeaders(): ?string + { + return $this->getHeaderLine('Access-Control-Allow-Headers'); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlRequestMethod(): ?string + { + return $this->getHeaderLine('Access-Control-Request-Method'); + } + + + protected function setStore($key, callable $callback) + { + + } + +} diff --git a/http-core/Message/OnDownload.php b/http-core/Message/OnDownload.php new file mode 100644 index 00000000..f7d749e8 --- /dev/null +++ b/http-core/Message/OnDownload.php @@ -0,0 +1,104 @@ + 'image/png', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'svg' => 'image/svg+xml', + ]; + + + /** + * @param string $path + * @param false $isChunk + * @param int $size + * @param int $offset + * @return $this + */ + public function path(string $path, bool $isChunk = false, int $size = -1, int $offset = 0): OnDownload + { + $this->path = $path; + $this->isChunk = $isChunk; + $this->size = $size; + $this->offset = $offset; + return $this->emitter(); + } + + + /** + * @return $this + */ + public function emitter(): static + { + $explode = explode('/', $this->path); + $this->withHeader('Pragma', 'public'); + $this->withHeader('Expires', '0'); + $this->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0'); + $this->withHeader('Content-Disposition', 'attachment;filename=' . end($explode)); + $this->withHeader('Content-Type', $type = get_file_extension($this->path)); + if (!in_array($type, self::IMAGES)) { + $this->withHeader('Content-Transfer-Encoding', 'binary'); + } + if ($this->isChunk) { + $resource = fopen($this->path, 'r'); + + $state = fstat($resource); + + $this->withHeader('Content-length', $state['size']); + } + return $this; + } + + + /** + * @param \Swoole\Http\Response $response + */ + public function dispatch(mixed $response) + { + if (!$this->isChunk) { + $response->sendfile($this->path); + } else { + $this->chunk($response); + } + } + + + /** + * @param \Swoole\Http\Response $response + */ + private function chunk(\Swoole\Http\Response $response): void + { + $resource = fopen($this->path, 'r'); + + $state = fstat($resource); + + $offset = $this->offset; + while ($file = fread($resource, $this->size)) { + $response->write($file); + fseek($resource, $offset); + if ($offset >= $state['size']) { + break; + } + $offset += $this->size; + } + $response->end(); + } +} diff --git a/http-core/Message/Parse.php b/http-core/Message/Parse.php new file mode 100644 index 00000000..90b91274 --- /dev/null +++ b/http-core/Message/Parse.php @@ -0,0 +1,35 @@ + Xml::toArray($content), + '[', '{' => json_decode($content, true), + default => call_user_func(function () use ($content) { + parse_str($content, $array); + return $array; + }) + }; + } + +} diff --git a/http-core/Message/Request.php b/http-core/Message/Request.php new file mode 100644 index 00000000..7f198052 --- /dev/null +++ b/http-core/Message/Request.php @@ -0,0 +1,115 @@ +authority = $authIdentity; + } + + + /** + * @return string + */ + public function getRequestTarget(): string + { + throw new BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param mixed $requestTarget + * @return static + */ + public function withRequestTarget($requestTarget): static + { + throw new BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @return string + */ + public function getMethod(): string + { + return $this->method; + } + + + /** + * @param string $method + * @return RequestInterface + */ + public function withMethod($method): RequestInterface + { + $this->method = $method; + return $this; + } + + + /** + * @param string $method + * @return bool + */ + public function isMethod(string $method): bool + { + return $this->method == $method; + } + + + /** + * @return UriInterface + */ + public function getUri(): UriInterface + { + return $this->uriInterface; + } + + + /** + * @param UriInterface $uri + * @param false $preserveHost + * @return $this|Request + */ + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface + { + $this->uriInterface = $uri; + return $this; + } +} diff --git a/http-core/Message/Response.php b/http-core/Message/Response.php new file mode 100644 index 00000000..071a5434 --- /dev/null +++ b/http-core/Message/Response.php @@ -0,0 +1,221 @@ +stream = new Stream(); + } + + + /** + * @return int + */ + public function getStatusCode(): int + { + return $this->statusCode; + } + + + /** + * @param int $code + * @param string $reasonPhrase + * @return $this|Response + */ + public function withStatus($code, $reasonPhrase = ''): static + { + $this->statusCode = $code; + $this->reasonPhrase = $reasonPhrase; + return $this; + } + + + /** + * @return string + */ + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowOrigin(): ?string + { + return $this->getHeaderLine('Access-Control-Allow-Origin'); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlAllowHeaders(): ?string + { + return $this->getHeaderLine('Access-Control-Allow-Headers'); + } + + + /** + * @return string|null + */ + #[Pure] public function getAccessControlRequestMethod(): ?string + { + return $this->getHeaderLine('Access-Control-Request-Method'); + } + + + /** + * @param string $type + * @return Response + */ + public function withContentType(string $type): static + { + return $this->withHeader('Content-Type', $type); + } + + + /** + * @return bool + */ + #[Pure] public function hasContentType(): bool + { + return $this->hasHeader('Content-Type'); + } + + /** + * @param string|null $value + * @return Response + */ + public function withAccessControlAllowHeaders(?string $value): static + { + return $this->withHeader('Access-Control-Allow-Headers', $value); + } + + + /** + * @param string|null $value + * @return Response + */ + public function withAccessControlRequestMethod(?string $value): static + { + return $this->withHeader('Access-Control-Request-Method', $value); + } + + + /** + * @param string|null $value + * @return Response + */ + public function withAccessControlAllowOrigin(?string $value): static + { + return $this->withHeader('Access-Control-Allow-Origin', $value); + } + + + /** + * @param $data + * @param string $contentType + * @return static + */ + public function json($data, string $contentType = 'application/json'): static + { + $this->stream->write(json_encode($data)); + + return $this->withContentType($contentType); + } + + + /** + * @param $data + * @param string $contentType + * @return static + */ + public function html($data, string $contentType = 'text/html'): static + { + if (!is_string($data)) { + $data = json_encode($data); + } + + $this->stream->write((string)$data); + + return $this->withContentType($contentType); + } + + + /** + * @param $data + * @param string $contentType + * @return static + */ + public function xml($data, string $contentType = 'application/xml'): static + { + $this->stream->write(Help::toXml($data)); + + return $this->withContentType($contentType); + } + + + /** + * @param string $charset + * @return $this + */ + public function withCharset(string $charset): static + { + $type = explode('charset', $this->getContentType())[0]; + $this->withContentType( + rtrim($type,';') . ';charset=' . $charset + ); + return $this; + } + + + /** + * @param $path + * @param bool $isChunk + * @param int $size + * @param int $offset + * @return OnDownloadInterface + * @throws Exception + */ + public function file($path, bool $isChunk = false, int $size = -1, int $offset = 0): OnDownloadInterface + { + $path = realpath($path); + if (!file_exists($path) || !is_readable($path)) { + throw new Exception('Cannot read file "' . $path . '", no permission'); + } + return (new OnDownload())->path($path, $isChunk, $size, $offset); + } +} diff --git a/http-core/Message/ServerRequest.php b/http-core/Message/ServerRequest.php new file mode 100644 index 00000000..46b7076a --- /dev/null +++ b/http-core/Message/ServerRequest.php @@ -0,0 +1,208 @@ +serverParams = $server; + return $this; + } + + /** + * @param \Swoole\Http\Request $server + * @return static + */ + public function withServerTarget(\Swoole\Http\Request $server): static + { + $this->serverTarget = $server; + return $this; + } + + + /** + * @param \Swoole\Http\Request $request + * @return static|ServerRequestInterface + * @throws \Exception + */ + public static function createServerRequest(\Swoole\Http\Request $request): static|ServerRequestInterface + { + $serverRequest = new ServerRequest(); + $serverRequest->withData($request->getData()); + $serverRequest->withServerParams($request->server); + $serverRequest->withServerTarget($request); + $serverRequest->withCookieParams($request->cookie); + $serverRequest->withUri(Uri::parseUri($request)); + $serverRequest->withQueryParams($request->get ?? []); + $serverRequest->withUploadedFiles($request->files ?? []); + $serverRequest->withMethod($request->getMethod()); + $serverRequest->withParsedBody($request->post); + return $serverRequest; + } + + + /** + * @return null|array + */ + public function getServerParams(): ?array + { + return $this->serverParams; + } + + + /** + * @return array|null + */ + public function getQueryParams(): ?array + { + return $this->queryParams; + } + + + /** + * @param array $query + * @return ServerRequestInterface + */ + public function withQueryParams(array $query): ServerRequestInterface + { + $this->queryParams = $query; + return $this; + } + + + /** + * @return array|null + */ + public function getUploadedFiles(): ?array + { + return $this->uploadedFiles; + } + + + /** + * @param array $uploadedFiles + * @return ServerRequestInterface + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + $this->uploadedFiles = $uploadedFiles; + return $this; + } + + + /** + * @return array|object|null + */ + public function getParsedBody(): object|array|null + { + if (empty($this->parsedBody)) { + $callback = Context::getContext(self::PARSE_BODY); + + $this->parsedBody = $callback($this->getBody(), $this->serverTarget->post); + } + return $this->parsedBody; + } + + + /** + * @param array|object|null $data + * @return ServerRequestInterface + */ + public function withParsedBody($data): ServerRequestInterface + { + $functions = function (StreamInterface $stream) use ($data) { + $content = Parse::data($stream->getContents()); + if (!empty($content)) { + return $content; + } + return $data; + }; + Context::setContext(self::PARSE_BODY, $functions); + return $this; + } + + + /** + * @return array + */ + public function getAttributes(): array + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param string $name + * @param null $default + * @return mixed + */ + public function getAttribute($name, $default = null): mixed + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param string $name + * @param mixed $value + * @return ServerRequestInterface + */ + public function withAttribute($name, $value): ServerRequestInterface + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param string $name + * @return ServerRequestInterface + */ + public function withoutAttribute($name): ServerRequestInterface + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } +} diff --git a/http-core/Message/StatusCode.php b/http-core/Message/StatusCode.php new file mode 100644 index 00000000..858168f4 --- /dev/null +++ b/http-core/Message/StatusCode.php @@ -0,0 +1,93 @@ + 'Continue 初始的请求已经接受,客户应当继续发送请求的其余部分。(HTTP 1.1新)', + self::CODE_101 => 'Switching Protocols 服务器将遵从客户的请求转换到另外一种协议(HTTP 1.1新)', + self::CODE_200 => '(成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。', + self::CODE_201 => '(已创建) 请求成功并且服务器创建了新的资源。', + self::CODE_202 => '(已接受) 服务器已接受请求,但尚未处理。', + self::CODE_203 => '(非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。', + self::CODE_204 => '(无内容) 服务器成功处理了请求,但没有返回任何内容。', + self::CODE_205 => '(重置内容) 服务器成功处理了请求,但没有返回任何内容。', + self::CODE_206 => '(部分内容) 服务器成功处理了部分 GET 请求。', + self::CODE_300 => '(多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。', + self::CODE_301 => '(永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。', + self::CODE_302 => '(临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。', + self::CODE_303 => '(查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。', + self::CODE_304 => '(未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。', + self::CODE_305 => '(使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。', + self::CODE_307 => '(临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。', + self::CODE_400 => '(错误请求) 服务器不理解请求的语法。', + self::CODE_401 => '(未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。', + self::CODE_403 => '(禁止) 服务器拒绝请求。', + self::CODE_404 => '(未找到) 服务器找不到请求的网页。', + self::CODE_405 => '(方法禁用) 禁用请求中指定的方法。', + self::CODE_406 => '(不接受) 无法使用请求的内容特性响应请求的网页。', + self::CODE_407 => '(需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。', + self::CODE_408 => '(请求超时) 服务器等候请求时发生超时。', + self::CODE_409 => '(冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。', + self::CODE_410 => '(已删除) 如果请求的资源已永久删除,服务器就会返回此响应。', + self::CODE_411 => '(需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。', + self::CODE_412 => '(未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。', + self::CODE_413 => '(请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。', + self::CODE_414 => '(请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。', + self::CODE_415 => '(不支持的媒体类型) 请求的格式不受请求页面的支持。', + self::CODE_416 => '(请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。', + self::CODE_417 => '(未满足期望值) 服务器未满足"期望"请求标头字段的要求。', + self::CODE_423 => ' 锁定的错误。', + self::CODE_500 => '(服务器内部错误) 服务器遇到错误,无法完成请求。', + self::CODE_501 => '(尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。', + self::CODE_502 => '(错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。', + self::CODE_503 => '(服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。', + self::CODE_504 => '(网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。', + self::CODE_505 => '(HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。', + ]; + +} diff --git a/http-core/Message/Stream.php b/http-core/Message/Stream.php new file mode 100644 index 00000000..11bd1a92 --- /dev/null +++ b/http-core/Message/Stream.php @@ -0,0 +1,237 @@ +content = $stream; + if (!is_resource($stream)) { + $this->size = strlen($stream); + } else { + $state = fstat($this->content); + if ($state) { + $this->size = $state['size']; + } + } + } + + + /** + * @return string + */ + public function __toString() + { + return $this->content; + } + + + /** + * + */ + public function close() + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @return resource|null + */ + public function detach() + { + if (!is_resource($this->content)) { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + $steam = stream_context_create(); + stream_copy_to_stream($this->content, $steam); + return $steam; + } + + + /** + * @return int + */ + public function getSize(): int + { + return $this->size; + } + + + /** + * @return bool|int + */ + public function tell(): bool|int + { + if (!is_resource($this->content)) { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + return ftell($this->content); + } + + + /** + * @return bool + */ + public function eof(): bool + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @return bool + */ + public function isSeekable(): bool + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param int $offset + * @param int $whence + */ + public function seek($offset, $whence = SEEK_SET) + { + if (!is_resource($this->content)) { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + fseek($this->content, $offset, $whence); + } + + + /** + * + */ + public function rewind() + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @return bool + */ + public function isWritable(): bool + { + if (!is_resource($this->content)) { + return true; + } + if (is_writable($this->content)) { + return true; + } + return false; + } + + + /** + * @param string $string + * @return int + */ + public function write($string): int + { + if (is_resource($this->content)) { + fwrite($this->content, $string); + $state = fstat($this->content); + if ($state) { + $this->size = $state['size']; + } + } else { + $this->content = $string; + $this->size = strlen($string); + } + return $this->size; + } + + + /** + * @return bool + */ + public function isReadable(): bool + { + if (!is_resource($this->content)) { + return true; + } + if (is_readable($this->content)) { + return true; + } + return false; + } + + + /** + * @param int $length + * @return false|string + */ + public function read($length): bool|string + { + if (is_resource($this->content)) { + return fread($this->content, $length); + } else { + return $this->content; + } + } + + + /** + * @return string|bool + */ + public function getContents(): string|bool + { + if (is_resource($this->content)) { + return stream_get_contents($this->content); + } else { + return $this->content; + } + } + + + /** + * @param null $key + * @return array + */ + #[ArrayShape([ + "timed_out" => "bool", + "blocked" => "bool", + "eof" => "bool", + "unread_bytes" => "int", + "stream_type" => "string", + "wrapper_type" => "string", + "wrapper_data" => "mixed", + "mode" => "string", + "seekable" => "bool", + "uri" => "string", + "crypto" => "array", + "mediatype" => "string", + ])] + public function getMetadata($key = null): array + { + if (is_resource($this->content)) { + return stream_get_meta_data($this->content); + } + throw new \BadMethodCallException('Not Accomplish Method.'); + } +} diff --git a/http-core/Message/Uploaded.php b/http-core/Message/Uploaded.php new file mode 100644 index 00000000..4d434ee4 --- /dev/null +++ b/http-core/Message/Uploaded.php @@ -0,0 +1,118 @@ + "There is no error, the file uploaded with success", + 1 => "The uploaded file exceeds the upload_max_filesize directive in php.ini", + 2 => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form", + 3 => "The uploaded file was only partially uploaded", + 4 => "No file was uploaded", + 6 => "Missing a temporary folder" + ]; + + + /** + * @var resource + */ + private mixed $stream; + + + /** + * @param string $tmp_name + * @param string $name + * @param string $type + * @param int $size + * @param int $error + */ + public function __construct( + public string $tmp_name, + public string $name, + public string $type, + public int $size, + public int $error + ) + { + } + + + /** + * @return StreamInterface + * @throws Exception + */ + public function getStream(): StreamInterface + { + if ($this->stream instanceof Stream) { + return $this->stream; + } + + $this->stream = new Stream(fopen($this->tmp_name, 'r+')); + + return $this->stream; + } + + + /** + * @param string $targetPath + * @return StreamInterface + * @throws Exception + */ + public function moveTo($targetPath): StreamInterface + { + @move_uploaded_file($this->tmp_name, $targetPath); + if (!file_exists($targetPath)) { + throw new Exception('File save fail.'); + } + + if ($this->stream instanceof Stream) { + $this->stream->close(); + $this->stream = null; + } + + $this->tmp_name = $targetPath; + return $this->getStream(); + } + + + /** + * @return int + */ + public function getSize(): int + { + return $this->size; + } + + /** + * @return int + */ + public function getError(): int + { + return $this->error; + } + + + /** + * @return string + */ + public function getClientFilename(): string + { + return $this->name; + } + + + /** + * @return string + */ + public function getClientMediaType(): string + { + return $this->type; + } +} diff --git a/http-core/Message/Uri.php b/http-core/Message/Uri.php new file mode 100644 index 00000000..68e610a0 --- /dev/null +++ b/http-core/Message/Uri.php @@ -0,0 +1,256 @@ +scheme; + } + + + /** + * @return string + */ + public function getAuthority(): string + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @return string + */ + public function getUserInfo(): string + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + /** + * @return string + */ + public function getHost(): string + { + return $this->host; + } + + + /** + * @return int|null + */ + public function getPort(): ?int + { + return $this->port; + } + + + /** + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + + /** + * @return string + */ + public function getQuery(): string + { + return $this->queryString; + } + + + /** + * @return string + */ + public function getFragment(): string + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param string $scheme + * @return $this|Uri + */ + public function withScheme($scheme): UriInterface + { + $this->scheme = $scheme; + return $this; + } + + + /** + * @param string $user + * @param null $password + * @return Uri + */ + public function withUserInfo($user, $password = null): UriInterface + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @param string $host + * @return $this|Uri + */ + public function withHost($host): UriInterface + { + $this->host = $host; + return $this; + } + + + /** + * @param int|null $port + * @return $this|Uri + */ + public function withPort($port): UriInterface + { + $this->port = $port; + return $this; + } + + + /** + * @param string $path + * @return $this|Uri + */ + public function withPath($path): UriInterface + { + $this->path = $path; + return $this; + } + + + /** + * @param string $query + * @return $this|Uri + */ + public function withQuery($query): UriInterface + { + $this->queryString = $query; + return $this; + } + + + /** + * @param string $fragment + * @return Uri + */ + public function withFragment($fragment): UriInterface + { + throw new \BadMethodCallException('Not Accomplish Method.'); + } + + + /** + * @return string + */ + public function __toString(): string + { + $domain = sprintf('%s://%s', $this->scheme, $this->host); + if (!in_array($this->port, [80, 443])) { + $domain .= ':' . $this->port; + } + if (empty($this->query) && empty($this->fragment)) { + return $domain . $this->path; + } + return sprintf('%s?%s#%s', $domain . $this->path, + $this->queryString, $this->fragment); + } + + + /** + * @return int + */ + public function getDefaultPort(): int + { + return $this->scheme == 'https' ? 443 : 80; + } + + + /** + * @param Request $request + * @return UriInterface + */ + public static function parseUri(Request $request): UriInterface + { + $server = $request->server; + $header = $request->header; + $uri = new static(); + $uri = $uri->withScheme(!empty($server['https']) && $server['https'] !== 'off' ? 'https' : 'http'); + if (isset($request->header['x-forwarded-proto'])) { + $uri->withScheme($request->header['x-forwarded-proto'])->withPort(443); + } + + $hasPort = false; + if (isset($server['http_host'])) { + $hostHeaderParts = explode(':', $server['http_host']); + $uri = $uri->withHost($hostHeaderParts[0]); + if (isset($hostHeaderParts[1])) { + $hasPort = true; + $uri = $uri->withPort($hostHeaderParts[1]); + } + } elseif (isset($server['server_name'])) { + $uri = $uri->withHost($server['server_name']); + } elseif (isset($server['server_addr'])) { + $uri = $uri->withHost($server['server_addr']); + } elseif (isset($header['host'])) { + $hasPort = true; + if (strpos($header['host'], ':')) { + [$host, $port] = explode(':', $header['host'], 2); + if ($port != $uri->getDefaultPort()) { + $uri = $uri->withPort($port); + } + } else { + $host = $header['host']; + } + + $uri = $uri->withHost($host); + } + + if (!$hasPort && isset($server['server_port'])) { + $uri = $uri->withPort($server['server_port']); + } + + $hasQuery = false; + if (isset($server['request_uri'])) { + $requestUriParts = explode('?', $server['request_uri']); + $uri = $uri->withPath($requestUriParts[0]); + if (isset($requestUriParts[1])) { + $hasQuery = true; + $uri = $uri->withQuery($requestUriParts[1]); + } + } + + if (!$hasQuery && isset($server['query_string'])) { + $uri = $uri->withQuery($server['query_string']); + } + + return $uri; + } +} diff --git a/http-core/OnDownloadInterface.php b/http-core/OnDownloadInterface.php new file mode 100644 index 00000000..6fd1ca3f --- /dev/null +++ b/http-core/OnDownloadInterface.php @@ -0,0 +1,12 @@ +