summaryrefslogtreecommitdiffhomepage
path: root/vhosts/t/phpcon-kagawa-2025/src
diff options
context:
space:
mode:
Diffstat (limited to 'vhosts/t/phpcon-kagawa-2025/src')
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/App.php61
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/Http/Response.php153
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/Http/Server.php169
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/Http/ServerRequest.php254
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/Http/Stream.php110
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieEatHandler.php66
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/CookieHandler.php92
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/GetHandler.php31
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/HealthHandler.php29
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/NotFoundHandler.php29
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/PhpConKagawa2025/PostHandler.php164
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));
+ }
+}