From 67094790d2d9db5c99e7c136f49061a78698e57d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 24 Nov 2025 04:58:38 +0900 Subject: Add vhosts/t/phpcon-kagawa-2025/ --- vhosts/t/phpcon-kagawa-2025/src/Http/Response.php | 153 +++++++++++++ vhosts/t/phpcon-kagawa-2025/src/Http/Server.php | 169 ++++++++++++++ .../phpcon-kagawa-2025/src/Http/ServerRequest.php | 254 +++++++++++++++++++++ vhosts/t/phpcon-kagawa-2025/src/Http/Stream.php | 110 +++++++++ 4 files changed, 686 insertions(+) create mode 100644 vhosts/t/phpcon-kagawa-2025/src/Http/Response.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/Http/Server.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/Http/ServerRequest.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/Http/Stream.php (limited to 'vhosts/t/phpcon-kagawa-2025/src/Http') diff --git a/vhosts/t/phpcon-kagawa-2025/src/Http/Response.php b/vhosts/t/phpcon-kagawa-2025/src/Http/Response.php new file mode 100644 index 0000000..4ab0d88 --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/Http/Response.php @@ -0,0 +1,153 @@ + 'OK', + 201 => 'Created', + 204 => 'No Content', + 301 => 'Moved Permanently', + 302 => 'Found', + 304 => 'Not Modified', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 500 => 'Internal Server Error', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + ]; + + public function __construct(int $statusCode = 200, array $headers = [], string $body = '', string $reasonPhrase = '') + { + $this->statusCode = $statusCode; + $this->reasonPhrase = $reasonPhrase !== '' ? $reasonPhrase : (self::$phrases[$statusCode] ?? ''); + $this->body = new Stream($body); + + foreach ($headers as $name => $value) { + $this->headers[strtolower($name)] = [ + 'name' => $name, + 'values' => is_array($value) ? $value : [$value], + ]; + } + } + + public function getProtocolVersion(): string + { + return $this->protocolVersion; + } + + public function withProtocolVersion(string $version): static + { + $clone = clone $this; + $clone->protocolVersion = $version; + return $clone; + } + + public function getHeaders(): array + { + $result = []; + foreach ($this->headers as $header) { + $result[$header['name']] = $header['values']; + } + return $result; + } + + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + public function getHeader(string $name): array + { + $lower = strtolower($name); + return $this->headers[$lower]['values'] ?? []; + } + + public function getHeaderLine(string $name): string + { + return implode(', ', $this->getHeader($name)); + } + + public function withHeader(string $name, $value): static + { + $clone = clone $this; + $clone->headers[strtolower($name)] = [ + 'name' => $name, + 'values' => is_array($value) ? $value : [$value], + ]; + return $clone; + } + + public function withAddedHeader(string $name, $value): static + { + $clone = clone $this; + $lower = strtolower($name); + $values = is_array($value) ? $value : [$value]; + + if (isset($clone->headers[$lower])) { + $clone->headers[$lower]['values'] = array_merge($clone->headers[$lower]['values'], $values); + } else { + $clone->headers[$lower] = [ + 'name' => $name, + 'values' => $values, + ]; + } + return $clone; + } + + public function withoutHeader(string $name): static + { + $clone = clone $this; + unset($clone->headers[strtolower($name)]); + return $clone; + } + + public function getBody(): StreamInterface + { + return $this->body; + } + + public function withBody(StreamInterface $body): static + { + $clone = clone $this; + $clone->body = $body; + return $clone; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function withStatus(int $code, string $reasonPhrase = ''): static + { + $clone = clone $this; + $clone->statusCode = $code; + $clone->reasonPhrase = $reasonPhrase !== '' ? $reasonPhrase : (self::$phrases[$code] ?? ''); + return $clone; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php b/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php new file mode 100644 index 0000000..cf13dcf --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php @@ -0,0 +1,169 @@ +host, $this->port) === false) { + throw new RuntimeException('socket_bind() failed: ' . socket_strerror(socket_last_error($socket))); + } + + if (socket_listen($socket, 5) === false) { + throw new RuntimeException('socket_listen() failed: ' . socket_strerror(socket_last_error($socket))); + } + + echo "HTTP server started on http://{$this->host}:{$this->port}\n"; + echo "Press Ctrl+C to stop\n\n"; + + for (; ;) { + $sock = socket_accept($socket); + if ($sock === false) { + echo 'socket_accept() failed: ' . socket_strerror(socket_last_error($socket)) . "\n"; + continue; + } + + $rawRequest = socket_read($sock, 8192); + + if ($rawRequest) { + $request = $this->parseRequest($rawRequest); + + echo 'Request: ' . $request->getMethod() . ' ' . $request->getRequestTarget() . "\n"; + + $response = $handler->handle($request); + + if (! $response->hasHeader('Connection')) { + $response = $response->withHeader('Connection', 'close'); + } + + $responseString = $this->responseToString($response); + socket_write($sock, $responseString, strlen($responseString)); + } + + socket_close($sock); + } + + // @phpstan-ignore deadCode.unreachable + socket_close($socket); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + return new Response($code, [], '', $reasonPhrase); + } + + public function createStream(string $content = ''): StreamInterface + { + return new Stream($content); + } + + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + throw new RuntimeException('Not implemented'); + } + + public function createStreamFromResource($resource): StreamInterface + { + throw new RuntimeException('Not implemented'); + } + + private function parseRequest(string $rawRequest): ServerRequest + { + $lines = explode("\r\n", $rawRequest); + $requestLine = trim($lines[0]); + + $parts = explode(' ', $requestLine); + $method = $parts[0]; + $path = $parts[1] ?? '/'; + + $headers = []; + $bodyStart = 0; + for ($i = 1; $i < count($lines); $i++) { + if ($lines[$i] === '') { + $bodyStart = $i + 1; + break; + } + $headerParts = explode(':', $lines[$i], 2); + if (count($headerParts) === 2) { + $headers[strtolower(trim($headerParts[0]))] = trim($headerParts[1]); + } + } + + $requestBody = ''; + if ($bodyStart > 0 && $bodyStart < count($lines)) { + $requestBody = implode("\r\n", array_slice($lines, $bodyStart)); + } + + $cookies = []; + if (isset($headers['cookie'])) { + $cookiePairs = explode(';', $headers['cookie']); + foreach ($cookiePairs as $pair) { + $kv = explode('=', trim($pair), 2); + if (count($kv) === 2) { + $cookies[$kv[0]] = urldecode($kv[1]); + } + } + } + + return new ServerRequest( + $method, + $path, + $headers, + $requestBody, + [], + $cookies, + ); + } + + private function responseToString(ResponseInterface $response): string + { + if (! $response->hasHeader('Content-Length')) { + $size = $response->getBody()->getSize(); + if ($size === null) { + throw new RuntimeException('Cannot determine body size for Content-Length header'); + } + $response = $response->withHeader('Content-Length', (string) $size); + } + + $result = sprintf( + "HTTP/%s %d %s\r\n", + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + $result .= "{$name}: {$value}\r\n"; + } + } + + $result .= "\r\n"; + $result .= (string) $response->getBody(); + + return $result; + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/Http/ServerRequest.php b/vhosts/t/phpcon-kagawa-2025/src/Http/ServerRequest.php new file mode 100644 index 0000000..4adec9d --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/Http/ServerRequest.php @@ -0,0 +1,254 @@ +method = $method; + $this->requestTarget = $requestTarget; + $this->body = new Stream($body); + $this->serverParams = $serverParams; + $this->cookieParams = $cookieParams; + $this->queryParams = $queryParams; + + $this->headers = []; + foreach ($headers as $name => $value) { + $this->headers[strtolower($name)] = [ + 'name' => $name, + 'values' => is_array($value) ? $value : [$value], + ]; + } + } + + public function getProtocolVersion(): string + { + return $this->protocolVersion; + } + + public function withProtocolVersion(string $version): static + { + $clone = clone $this; + $clone->protocolVersion = $version; + return $clone; + } + + public function getHeaders(): array + { + $result = []; + foreach ($this->headers as $header) { + $result[$header['name']] = $header['values']; + } + return $result; + } + + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + public function getHeader(string $name): array + { + $lower = strtolower($name); + return $this->headers[$lower]['values'] ?? []; + } + + public function getHeaderLine(string $name): string + { + return implode(', ', $this->getHeader($name)); + } + + public function withHeader(string $name, $value): static + { + $clone = clone $this; + $clone->headers[strtolower($name)] = [ + 'name' => $name, + 'values' => is_array($value) ? $value : [$value], + ]; + return $clone; + } + + public function withAddedHeader(string $name, $value): static + { + $clone = clone $this; + $lower = strtolower($name); + $values = is_array($value) ? $value : [$value]; + + if (isset($clone->headers[$lower])) { + $clone->headers[$lower]['values'] = array_merge($clone->headers[$lower]['values'], $values); + } else { + $clone->headers[$lower] = [ + 'name' => $name, + 'values' => $values, + ]; + } + return $clone; + } + + public function withoutHeader(string $name): static + { + $clone = clone $this; + unset($clone->headers[strtolower($name)]); + return $clone; + } + + public function getBody(): StreamInterface + { + return $this->body; + } + + public function withBody(StreamInterface $body): static + { + $clone = clone $this; + $clone->body = $body; + return $clone; + } + + public function getRequestTarget(): string + { + return $this->requestTarget; + } + + public function withRequestTarget(string $requestTarget): static + { + $clone = clone $this; + $clone->requestTarget = $requestTarget; + return $clone; + } + + public function getMethod(): string + { + return $this->method; + } + + public function withMethod(string $method): static + { + $clone = clone $this; + $clone->method = $method; + return $clone; + } + + public function getUri(): UriInterface + { + throw new RuntimeException('Not implemented'); + } + + public function withUri(UriInterface $uri, bool $preserveHost = false): static + { + throw new RuntimeException('Not implemented'); + } + + public function getServerParams(): array + { + return $this->serverParams; + } + + public function getCookieParams(): array + { + return $this->cookieParams; + } + + public function withCookieParams(array $cookies): static + { + $clone = clone $this; + $clone->cookieParams = $cookies; + return $clone; + } + + public function getQueryParams(): array + { + return $this->queryParams; + } + + public function withQueryParams(array $query): static + { + $clone = clone $this; + $clone->queryParams = $query; + return $clone; + } + + public function getUploadedFiles(): array + { + return $this->uploadedFiles; + } + + public function withUploadedFiles(array $uploadedFiles): static + { + $clone = clone $this; + $clone->uploadedFiles = $uploadedFiles; + return $clone; + } + + public function getParsedBody(): array|object|null + { + return $this->parsedBody; + } + + public function withParsedBody($data): static + { + $clone = clone $this; + $clone->parsedBody = $data; + return $clone; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getAttribute(string $name, $default = null): mixed + { + return $this->attributes[$name] ?? $default; + } + + public function withAttribute(string $name, $value): static + { + $clone = clone $this; + $clone->attributes[$name] = $value; + return $clone; + } + + public function withoutAttribute(string $name): static + { + $clone = clone $this; + unset($clone->attributes[$name]); + return $clone; + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/Http/Stream.php b/vhosts/t/phpcon-kagawa-2025/src/Http/Stream.php new file mode 100644 index 0000000..adcdd7d --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/Http/Stream.php @@ -0,0 +1,110 @@ +content = $content; + } + + public function __toString(): string + { + return $this->content; + } + + public function close(): void + { + $this->content = ''; + $this->position = 0; + } + + public function detach(): null + { + return null; + } + + public function getSize(): int + { + return strlen($this->content); + } + + public function tell(): int + { + return $this->position; + } + + public function eof(): bool + { + return $this->position >= strlen($this->content); + } + + public function isSeekable(): bool + { + return true; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + switch ($whence) { + case SEEK_SET: + $this->position = $offset; + break; + case SEEK_CUR: + $this->position += $offset; + break; + case SEEK_END: + $this->position = strlen($this->content) + $offset; + break; + } + } + + public function rewind(): void + { + $this->position = 0; + } + + public function isWritable(): bool + { + return true; + } + + public function write(string $string): int + { + $this->content .= $string; + return strlen($string); + } + + public function isReadable(): bool + { + return true; + } + + public function read(int $length): string + { + $result = substr($this->content, $this->position, $length); + $this->position += strlen($result); + return $result; + } + + public function getContents(): string + { + $result = substr($this->content, $this->position); + $this->position = strlen($this->content); + return $result; + } + + public function getMetadata(?string $key = null): ?array + { + return $key === null ? [] : null; + } +} -- cgit v1.2.3-70-g09d2