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/App.php | 61 +++++ 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 +++++++++ .../src/PhpConKagawa2025/CookieEatHandler.php | 66 ++++++ .../src/PhpConKagawa2025/CookieHandler.php | 92 ++++++++ .../src/PhpConKagawa2025/GetHandler.php | 31 +++ .../src/PhpConKagawa2025/HealthHandler.php | 29 +++ .../src/PhpConKagawa2025/NotFoundHandler.php | 29 +++ .../src/PhpConKagawa2025/PostHandler.php | 164 +++++++++++++ 11 files changed, 1158 insertions(+) create mode 100644 vhosts/t/phpcon-kagawa-2025/src/App.php 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 create mode 100644 vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieEatHandler.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieHandler.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/GetHandler.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/HealthHandler.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/NotFoundHandler.php create mode 100644 vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/PostHandler.php (limited to 'vhosts/t/phpcon-kagawa-2025/src') diff --git a/vhosts/t/phpcon-kagawa-2025/src/App.php b/vhosts/t/phpcon-kagawa-2025/src/App.php new file mode 100644 index 0000000..85d5ed4 --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/App.php @@ -0,0 +1,61 @@ + + */ + private array $routes = []; + + private RequestHandlerInterface $notFoundHandler; + + public function __construct(string $host, int $port) + { + $this->server = new Server($host, $port); + $this->notFoundHandler = new NotFoundHandler($this->server, $this->server); + } + + public function run(): void + { + $this->addRoute('/phpcon-kagawa-2025/health/', new HealthHandler($this->server, $this->server)); + $this->addRoute('/phpcon-kagawa-2025/get/', new GetHandler($this->server, $this->server)); + $this->addRoute('/phpcon-kagawa-2025/post/', new PostHandler($this->server, $this->server)); + $this->addRoute('/phpcon-kagawa-2025/cookie/', new CookieHandler($this->server, $this->server)); + $this->addRoute('/phpcon-kagawa-2025/cookie/eat/', new CookieEatHandler($this->server, $this->server)); + + $this->server->run($this); + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $path = $request->getRequestTarget(); + if ($path !== '/' && ! str_ends_with($path, '/')) { + $path .= '/'; + } + $handler = $this->routes[$path] ?? $this->notFoundHandler; + return $handler->handle($request); + } + + private function addRoute(string $path, RequestHandlerInterface $handler): self + { + $this->routes[$path] = $handler; + return $this; + } +} 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; + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieEatHandler.php b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieEatHandler.php new file mode 100644 index 0000000..93e9c3b --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieEatHandler.php @@ -0,0 +1,66 @@ +getCookieParams(); + + $orders = []; + if (isset($cookies['order']) && $cookies['order'] !== '') { + $orders = explode(',', $cookies['order']); + } + + if (count($orders) > 0) { + $orderList = ''; + $message = '

ごちそうさまでした。

'; + } else { + $orderList = ''; + $message = '

注文がありません。

'; + } + + $body = << + + + + + いただきます + + +
+

いただきます

+ {$orderList} + {$message} + もう一度注文する +
+ + +HTML; + + return $this->responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/html; charset=UTF-8') + ->withHeader('Set-Cookie', 'order=; Max-Age=0; path=/') + ->withBody($this->streamFactory->createStream($body)); + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieHandler.php b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieHandler.php new file mode 100644 index 0000000..0b9f656 --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieHandler.php @@ -0,0 +1,92 @@ +getCookieParams(); + + $orders = []; + if (isset($cookies['order']) && $cookies['order'] !== '') { + $orders = explode(',', $cookies['order']); + } + + $setCookie = null; + + if ($request->getMethod() === 'POST') { + $postData = []; + parse_str((string) $request->getBody(), $postData); + $newOrder = $postData['udon'] ?? ''; + if ($newOrder !== '') { + $orders[] = $newOrder; + $setCookie = 'order=' . urlencode(implode(',', $orders)) . '; path=/'; + } + } + + $orderList = ''; + if (count($orders) > 0) { + $orderList = '

現在の注文

'; + $orderList .= '食べる'; + } else { + $orderList = '

まだ注文がありません。

'; + } + + $body = << + + + + + うどん店 + + +
+

うどん店

+
+
+ +
+ +
+ {$orderList} +
+ + +HTML; + + $response = $this->responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/html; charset=UTF-8') + ->withBody($this->streamFactory->createStream($body)); + + if ($setCookie !== null) { + $response = $response->withHeader('Set-Cookie', $setCookie); + } + + return $response; + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/GetHandler.php b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/GetHandler.php new file mode 100644 index 0000000..555ef79 --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/GetHandler.php @@ -0,0 +1,31 @@ +responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/plain; charset=UTF-8') + ->withBody($this->streamFactory->createStream($body)); + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/HealthHandler.php b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/HealthHandler.php new file mode 100644 index 0000000..2c2d1b5 --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/HealthHandler.php @@ -0,0 +1,29 @@ +responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/plain; charset=UTF-8') + ->withBody($this->streamFactory->createStream($body)); + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/NotFoundHandler.php b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/NotFoundHandler.php new file mode 100644 index 0000000..0d5b33f --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/NotFoundHandler.php @@ -0,0 +1,29 @@ +responseFactory->createResponse(404) + ->withHeader('Content-Type', 'text/plain; charset=UTF-8') + ->withBody($this->streamFactory->createStream($body)); + } +} diff --git a/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/PostHandler.php b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/PostHandler.php new file mode 100644 index 0000000..2653e3e --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/PostHandler.php @@ -0,0 +1,164 @@ + + */ + private const array ANSWERS = [ + 'ehime' => 'hiroshima', + 'kagawa' => 'okayama', + 'tokushima' => 'hyogo', + ]; + + /** + * @var array + */ + private const array PREFECTURE_NAMES = [ + 'okayama' => '岡山', + 'hiroshima' => '広島', + 'yamaguchi' => '山口', + 'hyogo' => '兵庫', + 'osaka' => '大阪', + ]; + + private const string QUIZ_FORM = <<<'HTML' + + + + + + 本州四国連絡橋クイズ + + +
+

本州四国連絡橋クイズ

+

四国の各県と橋でつながっている中国地方の県を選んでください。

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +HTML; + + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + if ($request->getMethod() === 'POST') { + $postData = []; + parse_str((string) $request->getBody(), $postData); + + $userEhime = $postData['ehime'] ?? ''; + $userKagawa = $postData['kagawa'] ?? ''; + $userTokushima = $postData['tokushima'] ?? ''; + + $results = []; + $score = 0; + + // Ehime + $userEhimeName = self::PREFECTURE_NAMES[$userEhime] ?? $userEhime; + if ($userEhime === self::ANSWERS['ehime']) { + $results[] = '

愛媛: ' . htmlspecialchars($userEhimeName) . ' ⭕ 正解!

'; + $score++; + } else { + $results[] = '

愛媛: ' . htmlspecialchars($userEhimeName) . ' ❌ 不正解

'; + } + + // Kagawa + $userKagawaName = self::PREFECTURE_NAMES[$userKagawa] ?? $userKagawa; + if ($userKagawa === self::ANSWERS['kagawa']) { + $results[] = '

香川: ' . htmlspecialchars($userKagawaName) . ' ⭕ 正解!

'; + $score++; + } else { + $results[] = '

香川: ' . htmlspecialchars($userKagawaName) . ' ❌ 不正解

'; + } + + // Tokushima + $userTokushimaName = self::PREFECTURE_NAMES[$userTokushima] ?? $userTokushima; + if ($userTokushima === self::ANSWERS['tokushima']) { + $results[] = '

徳島: ' . htmlspecialchars($userTokushimaName) . ' ⭕ 正解!

'; + $score++; + } else { + $results[] = '

徳島: ' . htmlspecialchars($userTokushimaName) . ' ❌ 不正解

'; + } + + $resultHtml = implode("\n", $results); + + $body = << + + + + + クイズ結果 + + +
+

クイズ結果

+

スコア: {$score} / 3

+ {$resultHtml} +

+ もう一度挑戦する +

+
+ + +HTML; + } else { + $body = self::QUIZ_FORM; + } + + return $this->responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/html; charset=UTF-8') + ->withBody($this->streamFactory->createStream($body)); + } +} -- cgit v1.2.3-70-g09d2