diff options
Diffstat (limited to 'vhosts/t/phpcon-kagawa-2025/src')
11 files changed, 1158 insertions, 0 deletions
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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd; + +use Nsfisis\TinyPhpHttpd\Http\Server; +use Nsfisis\TinyPhpHttpd\PhpConKagawa2025\CookieEatHandler; +use Nsfisis\TinyPhpHttpd\PhpConKagawa2025\CookieHandler; +use Nsfisis\TinyPhpHttpd\PhpConKagawa2025\GetHandler; +use Nsfisis\TinyPhpHttpd\PhpConKagawa2025\HealthHandler; +use Nsfisis\TinyPhpHttpd\PhpConKagawa2025\NotFoundHandler; +use Nsfisis\TinyPhpHttpd\PhpConKagawa2025\PostHandler; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final class App implements RequestHandlerInterface +{ + private Server $server; + + /** + * @var array<string, RequestHandlerInterface> + */ + 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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\Http; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; + +final class Response implements ResponseInterface +{ + private int $statusCode; + + private string $reasonPhrase; + + private array $headers = []; + + private StreamInterface $body; + + private string $protocolVersion = '1.1'; + + private static array $phrases = [ + 200 => '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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\Http; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Server\RequestHandlerInterface; +use RuntimeException; + +final readonly class Server implements ResponseFactoryInterface, StreamFactoryInterface +{ + public function __construct( + private string $host, + private int $port + ) { + } + + public function run(RequestHandlerInterface $handler): void + { + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($socket === false) { + throw new RuntimeException('socket_create() failed: ' . socket_strerror(socket_last_error())); + } + + socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); + + if (socket_bind($socket, $this->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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\Http; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; +use RuntimeException; + +final class ServerRequest implements ServerRequestInterface +{ + private string $method; + + private string $requestTarget; + + private array $headers; + + private StreamInterface $body; + + private string $protocolVersion = '1.1'; + + private array $serverParams; + + private array $cookieParams; + + private array $queryParams; + + private array $uploadedFiles = []; + + private array|object|null $parsedBody = null; + + private array $attributes = []; + + public function __construct( + string $method, + string $requestTarget, + array $headers = [], + string $body = '', + array $serverParams = [], + array $cookieParams = [], + array $queryParams = [] + ) { + $this->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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\Http; + +use Psr\Http\Message\StreamInterface; + +final class Stream implements StreamInterface +{ + private string $content; + + private int $position = 0; + + public function __construct(string $content = '') + { + $this->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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\PhpConKagawa2025; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final readonly class CookieEatHandler implements RequestHandlerInterface +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $cookies = $request->getCookieParams(); + + $orders = []; + if (isset($cookies['order']) && $cookies['order'] !== '') { + $orders = explode(',', $cookies['order']); + } + + if (count($orders) > 0) { + $orderList = '<ul>'; + foreach ($orders as $order) { + $orderList .= '<li>' . htmlspecialchars($order) . '</li>'; + } + $orderList .= '</ul>'; + $message = '<p>ごちそうさまでした。</p>'; + } else { + $orderList = ''; + $message = '<p>注文がありません。</p>'; + } + + $body = <<<HTML +<!DOCTYPE html> +<html lang="ja"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>いただきます</title> +</head> +<body> + <div class="container"> + <h1>いただきます</h1> + {$orderList} + {$message} + <a href="/phpcon-kagawa-2025/cookie/">もう一度注文する</a> + </div> +</body> +</html> +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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\PhpConKagawa2025; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final readonly class CookieHandler implements RequestHandlerInterface +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $cookies = $request->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 = '<h2>現在の注文</h2><ul>'; + foreach ($orders as $order) { + $orderList .= '<li>' . htmlspecialchars($order) . '</li>'; + } + $orderList .= '</ul>'; + $orderList .= '<a href="/phpcon-kagawa-2025/cookie/eat/">食べる</a>'; + } else { + $orderList = '<p>まだ注文がありません。</p>'; + } + + $body = <<<HTML +<!DOCTYPE html> +<html lang="ja"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>うどん店</title> +</head> +<body> + <main> + <h1>うどん店</h1> + <form method="POST" action="/cookie/"> + <div> + <select name="udon"> + <option value="かけ">かけ</option> + <option value="ぶっかけ">ぶっかけ</option> + <option value="釜揚げ">釜揚げ</option> + <option value="釜玉">釜玉</option> + </select> + </div> + <button type="submit">注文</button> + </form> + {$orderList} + </main> +</body> +</html> +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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\PhpConKagawa2025; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final readonly class GetHandler implements RequestHandlerInterface +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + ob_start(); + phpinfo(INFO_GENERAL | INFO_CREDITS | INFO_LICENSE); + $body = ob_get_clean(); + + return $this->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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\PhpConKagawa2025; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final readonly class HealthHandler implements RequestHandlerInterface +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $body = 'OK'; + + return $this->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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\PhpConKagawa2025; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final readonly class NotFoundHandler implements RequestHandlerInterface +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $body = '404 Not Found'; + + return $this->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 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\PhpConKagawa2025; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final readonly class PostHandler implements RequestHandlerInterface +{ + /** + * @var array<string, string> + */ + private const array ANSWERS = [ + 'ehime' => 'hiroshima', + 'kagawa' => 'okayama', + 'tokushima' => 'hyogo', + ]; + + /** + * @var array<string, string> + */ + private const array PREFECTURE_NAMES = [ + 'okayama' => '岡山', + 'hiroshima' => '広島', + 'yamaguchi' => '山口', + 'hyogo' => '兵庫', + 'osaka' => '大阪', + ]; + + private const string QUIZ_FORM = <<<'HTML' +<!DOCTYPE html> +<html lang="ja"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>本州四国連絡橋クイズ</title> +</head> +<body> + <main> + <h1>本州四国連絡橋クイズ</h1> + <p>四国の各県と橋でつながっている中国地方の県を選んでください。</p> + <form method="POST" action="/post/"> + <div> + <label>愛媛とつながっているのは?</label> + <select name="ehime"> + <option value="">選択してください</option> + <option value="okayama">岡山</option> + <option value="hiroshima">広島</option> + <option value="yamaguchi">山口</option> + <option value="hyogo">兵庫</option> + <option value="osaka">大阪</option> + </select> + </div> + <div> + <label>香川とつながっているのは?</label> + <select name="kagawa"> + <option value="">選択してください</option> + <option value="okayama">岡山</option> + <option value="hiroshima">広島</option> + <option value="yamaguchi">山口</option> + <option value="hyogo">兵庫</option> + <option value="osaka">大阪</option> + </select> + </div> + <div> + <label>徳島とつながっているのは?</label> + <select name="tokushima"> + <option value="">選択してください</option> + <option value="okayama">岡山</option> + <option value="hiroshima">広島</option> + <option value="yamaguchi">山口</option> + <option value="hyogo">兵庫</option> + <option value="osaka">大阪</option> + </select> + </div> + <button type="submit">回答する</button> + </form> + </main> +</body> +</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[] = '<p>愛媛: ' . htmlspecialchars($userEhimeName) . ' ⭕ 正解!</p>'; + $score++; + } else { + $results[] = '<p>愛媛: ' . htmlspecialchars($userEhimeName) . ' ❌ 不正解</p>'; + } + + // Kagawa + $userKagawaName = self::PREFECTURE_NAMES[$userKagawa] ?? $userKagawa; + if ($userKagawa === self::ANSWERS['kagawa']) { + $results[] = '<p>香川: ' . htmlspecialchars($userKagawaName) . ' ⭕ 正解!</p>'; + $score++; + } else { + $results[] = '<p>香川: ' . htmlspecialchars($userKagawaName) . ' ❌ 不正解</p>'; + } + + // Tokushima + $userTokushimaName = self::PREFECTURE_NAMES[$userTokushima] ?? $userTokushima; + if ($userTokushima === self::ANSWERS['tokushima']) { + $results[] = '<p>徳島: ' . htmlspecialchars($userTokushimaName) . ' ⭕ 正解!</p>'; + $score++; + } else { + $results[] = '<p>徳島: ' . htmlspecialchars($userTokushimaName) . ' ❌ 不正解</p>'; + } + + $resultHtml = implode("\n", $results); + + $body = <<<HTML +<!DOCTYPE html> +<html lang="ja"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>クイズ結果</title> +</head> +<body> + <main> + <h1>クイズ結果</h1> + <p class="score">スコア: {$score} / 3</p> + {$resultHtml} + <p> + <a href="/phpcon-kagawa-2025/post/">もう一度挑戦する</a> + </p> + </main> +</body> +</html> +HTML; + } else { + $body = self::QUIZ_FORM; + } + + return $this->responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/html; charset=UTF-8') + ->withBody($this->streamFactory->createStream($body)); + } +} |
