summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-11-24 04:58:38 +0900
committernsfisis <nsfisis@gmail.com>2025-11-24 04:58:38 +0900
commit67094790d2d9db5c99e7c136f49061a78698e57d (patch)
tree02feb966e74c7c2d1b6a77d8310502aa9758649b
parenta071111365f9760b2f97fa3f6e12aee9f75dd15d (diff)
downloadnil.ninja-67094790d2d9db5c99e7c136f49061a78698e57d.tar.gz
nil.ninja-67094790d2d9db5c99e7c136f49061a78698e57d.tar.zst
nil.ninja-67094790d2d9db5c99e7c136f49061a78698e57d.zip
Add vhosts/t/phpcon-kagawa-2025/
-rw-r--r--Makefile3
-rw-r--r--mioproxy.prod.hcl11
-rw-r--r--vhosts/t/phpcon-kagawa-2025/.dockerignore1
-rw-r--r--vhosts/t/phpcon-kagawa-2025/.editorconfig9
-rw-r--r--vhosts/t/phpcon-kagawa-2025/.gitignore1
-rw-r--r--vhosts/t/phpcon-kagawa-2025/Dockerfile21
-rw-r--r--vhosts/t/phpcon-kagawa-2025/Makefile.prod27
-rw-r--r--vhosts/t/phpcon-kagawa-2025/compose.yaml6
-rw-r--r--vhosts/t/phpcon-kagawa-2025/composer.json39
-rw-r--r--vhosts/t/phpcon-kagawa-2025/composer.lock441
-rw-r--r--vhosts/t/phpcon-kagawa-2025/ecs.php17
-rw-r--r--vhosts/t/phpcon-kagawa-2025/index.php9
-rw-r--r--vhosts/t/phpcon-kagawa-2025/phpstan.neon5
-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
-rwxr-xr-xvhosts/t/phpcon-kagawa-2025/test.sh110
-rwxr-xr-xvhosts/t/phpcon-kagawa-2025/tests/test_404.sh61
-rwxr-xr-xvhosts/t/phpcon-kagawa-2025/tests/test_cookie.sh85
-rwxr-xr-xvhosts/t/phpcon-kagawa-2025/tests/test_cookie_eat.sh83
-rwxr-xr-xvhosts/t/phpcon-kagawa-2025/tests/test_get.sh49
-rwxr-xr-xvhosts/t/phpcon-kagawa-2025/tests/test_post.sh75
30 files changed, 2211 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index a631397..71103e7 100644
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,7 @@ build:
cd vhosts/t/albatross-swift; make -f Makefile.prod build
cd vhosts/t/albatross-php-2025; make -f Makefile.prod build
cd vhosts/t/albatross-swift-2025; make -f Makefile.prod build
+ cd vhosts/t/phpcon-kagawa-2025; make -f Makefile.prod build
.PHONY: serve
serve:
@@ -22,9 +23,11 @@ serve:
cd vhosts/t/albatross-swift; make -f Makefile.prod serve
cd vhosts/t/albatross-php-2025; make -f Makefile.prod serve
cd vhosts/t/albatross-swift-2025; make -f Makefile.prod serve
+ cd vhosts/t/phpcon-kagawa-2025; make -f Makefile.prod serve
.PHONY: clean
clean:
+ cd vhosts/t/phpcon-kagawa-2025; make -f Makefile.prod clean
cd vhosts/t/albatross-swift-2025; make -f Makefile.prod clean
cd vhosts/t/albatross-php-2025; make -f Makefile.prod clean
cd vhosts/t/albatross-swift; make -f Makefile.prod clean
diff --git a/mioproxy.prod.hcl b/mioproxy.prod.hcl
index 45162cb..7a31dbb 100644
--- a/mioproxy.prod.hcl
+++ b/mioproxy.prod.hcl
@@ -60,4 +60,15 @@ server https {
port = 8004
}
}
+
+ proxy phpcon-kagawa-2025 {
+ from {
+ host = "t.nil.ninja"
+ path = "/phpcon-kagawa-2025/"
+ }
+ to {
+ host = "127.0.0.1"
+ port = 8005
+ }
+ }
}
diff --git a/vhosts/t/phpcon-kagawa-2025/.dockerignore b/vhosts/t/phpcon-kagawa-2025/.dockerignore
new file mode 100644
index 0000000..61ead86
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/.dockerignore
@@ -0,0 +1 @@
+/vendor
diff --git a/vhosts/t/phpcon-kagawa-2025/.editorconfig b/vhosts/t/phpcon-kagawa-2025/.editorconfig
new file mode 100644
index 0000000..e291365
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/vhosts/t/phpcon-kagawa-2025/.gitignore b/vhosts/t/phpcon-kagawa-2025/.gitignore
new file mode 100644
index 0000000..61ead86
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/.gitignore
@@ -0,0 +1 @@
+/vendor
diff --git a/vhosts/t/phpcon-kagawa-2025/Dockerfile b/vhosts/t/phpcon-kagawa-2025/Dockerfile
new file mode 100644
index 0000000..eb436ec
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/Dockerfile
@@ -0,0 +1,21 @@
+FROM php:8.4-cli
+
+RUN apt-get update && \
+ apt-get install -y git unzip
+
+RUN docker-php-ext-configure sockets && \
+ docker-php-ext-install -j$(nproc) sockets
+
+COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+
+WORKDIR /app
+
+COPY composer.json composer.lock ./
+
+RUN composer install --no-dev --optimize-autoloader --no-interaction
+
+COPY . .
+
+EXPOSE 8080
+
+CMD ["php", "index.php"]
diff --git a/vhosts/t/phpcon-kagawa-2025/Makefile.prod b/vhosts/t/phpcon-kagawa-2025/Makefile.prod
new file mode 100644
index 0000000..6172444
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/Makefile.prod
@@ -0,0 +1,27 @@
+DOCKER_COMPOSE := docker compose
+
+.PHONY: build
+build:
+ ${DOCKER_COMPOSE} build
+
+.PHONY: serve
+serve: up
+
+.PHONY: clean
+clean: down
+
+.PHONY: up
+up:
+ ${DOCKER_COMPOSE} up -d
+
+.PHONY: down
+down:
+ ${DOCKER_COMPOSE} down --remove-orphans
+
+.PHONY: logs
+logs:
+ ${DOCKER_COMPOSE} logs
+
+.PHONY: logsf
+logsf:
+ ${DOCKER_COMPOSE} logs -f
diff --git a/vhosts/t/phpcon-kagawa-2025/compose.yaml b/vhosts/t/phpcon-kagawa-2025/compose.yaml
new file mode 100644
index 0000000..5519689
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/compose.yaml
@@ -0,0 +1,6 @@
+services:
+ app:
+ build: .
+ ports:
+ - '127.0.0.1:8005:8080'
+ restart: always
diff --git a/vhosts/t/phpcon-kagawa-2025/composer.json b/vhosts/t/phpcon-kagawa-2025/composer.json
new file mode 100644
index 0000000..6153333
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "nsfisis/tiny-php-httpd",
+ "type": "project",
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "Nsfisis\\TinyPhpHttpd\\": "src/"
+ }
+ },
+ "authors": [
+ {
+ "name": "nsfisis"
+ }
+ ],
+ "require": {
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^2.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "symplify/easy-coding-standard": "^13.0"
+ },
+ "config": {
+ "allow-plugins": {
+ "phpstan/extension-installer": true
+ },
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "scripts": {
+ "ecs": "ecs check",
+ "ecsfix": "ecs check --fix",
+ "phpstan": "phpstan analyse"
+ }
+}
diff --git a/vhosts/t/phpcon-kagawa-2025/composer.lock b/vhosts/t/phpcon-kagawa-2025/composer.lock
new file mode 100644
index 0000000..2637d25
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/composer.lock
@@ -0,0 +1,441 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "b5f68eee17055b1ffab24a699a46508d",
+ "packages": [
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/http-server-handler",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-handler.git",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side request handler",
+ "keywords": [
+ "handler",
+ "http",
+ "http-interop",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response",
+ "server"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:06:20+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "phpstan/extension-installer",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/extension-installer.git",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.9.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2.0",
+ "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPStan\\ExtensionInstaller\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\ExtensionInstaller\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Composer plugin for automatic installation of PHPStan extensions",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpstan/extension-installer/issues",
+ "source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
+ },
+ "time": "2024-09-04T20:21:43+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.32",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227",
+ "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-11T15:18:17+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-deprecation-rules",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-deprecation-rules.git",
+ "reference": "468e02c9176891cc901143da118f09dc9505fc2f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f",
+ "reference": "468e02c9176891cc901143da118f09dc9505fc2f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.1.15"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues",
+ "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3"
+ },
+ "time": "2025-05-14T10:56:57+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-strict-rules",
+ "version": "2.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-strict-rules.git",
+ "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538",
+ "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.1.29"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Extra strict and opinionated rules for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
+ "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7"
+ },
+ "time": "2025-09-26T11:19:08+00:00"
+ },
+ {
+ "name": "symplify/easy-coding-standard",
+ "version": "13.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/easy-coding-standard/easy-coding-standard.git",
+ "reference": "24708c6673871e342245c692e1bb304f119ffc58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/24708c6673871e342245c692e1bb304f119ffc58",
+ "reference": "24708c6673871e342245c692e1bb304f119ffc58",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "conflict": {
+ "friendsofphp/php-cs-fixer": "<3.46",
+ "phpcsstandards/php_codesniffer": "<3.8",
+ "symplify/coding-standard": "<12.1"
+ },
+ "suggest": {
+ "ext-dom": "Needed to support checkstyle output format in class CheckstyleOutputFormatter"
+ },
+ "bin": [
+ "bin/ecs"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer",
+ "keywords": [
+ "Code style",
+ "automation",
+ "fixer",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues",
+ "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/13.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/rectorphp",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/tomasvotruba",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-06T14:47:06+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
+}
diff --git a/vhosts/t/phpcon-kagawa-2025/ecs.php b/vhosts/t/phpcon-kagawa-2025/ecs.php
new file mode 100644
index 0000000..bf622ea
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/ecs.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+use Symplify\EasyCodingStandard\Config\ECSConfig;
+use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
+
+return function (ECSConfig $ecsConfig): void {
+ $ecsConfig->paths([
+ __DIR__ . '/src',
+ ]);
+ $ecsConfig->sets([
+ SetList::COMMON,
+ SetList::CLEAN_CODE,
+ SetList::PSR_12,
+ ]);
+};
diff --git a/vhosts/t/phpcon-kagawa-2025/index.php b/vhosts/t/phpcon-kagawa-2025/index.php
new file mode 100644
index 0000000..e2f5829
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/index.php
@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+require_once __DIR__ . '/vendor/autoload.php';
+
+use Nsfisis\TinyPhpHttpd\App;
+
+(new App('0.0.0.0', 8080))->run();
diff --git a/vhosts/t/phpcon-kagawa-2025/phpstan.neon b/vhosts/t/phpcon-kagawa-2025/phpstan.neon
new file mode 100644
index 0000000..fe4daf0
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/phpstan.neon
@@ -0,0 +1,5 @@
+parameters:
+ level: 4
+ tipsOfTheDay: false
+ paths:
+ - 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 @@
+<?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));
+ }
+}
diff --git a/vhosts/t/phpcon-kagawa-2025/test.sh b/vhosts/t/phpcon-kagawa-2025/test.sh
new file mode 100755
index 0000000..b3addf1
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/test.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+# Main test runner for http_server.php
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+SERVER_PID=""
+PORT=8080
+HOST=127.0.0.1
+BASE_URL="http://${HOST}:${PORT}/phpcon-kagawa-2025"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+cleanup() {
+ if [ -n "$SERVER_PID" ]; then
+ echo ""
+ echo "Stopping server (PID: $SERVER_PID)..."
+ kill $SERVER_PID 2>/dev/null || true
+ wait $SERVER_PID 2>/dev/null || true
+ echo "Server stopped."
+ fi
+}
+
+trap cleanup EXIT
+
+echo "================================"
+echo "HTTP Server Test Suite"
+echo "================================"
+echo ""
+
+# Check if port is already in use
+if lsof -i :$PORT -t >/dev/null 2>&1; then
+ echo -e "${RED}Error: Port $PORT is already in use${NC}"
+ exit 1
+fi
+
+# Start the server
+echo "Starting HTTP server..."
+php "$SCRIPT_DIR/index.php" > /dev/null 2>&1 &
+SERVER_PID=$!
+
+# Wait for server to start using health check endpoint
+echo -n "Waiting for server to be ready"
+for i in {1..30}; do
+ if curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health" 2>/dev/null | grep -q "200"; then
+ echo " OK"
+ break
+ fi
+ echo -n "."
+ sleep 0.2
+done
+
+# Verify server is running
+if ! curl -s -o /dev/null "$BASE_URL/health" 2>/dev/null; then
+ echo -e "${RED}Error: Server failed to start${NC}"
+ exit 1
+fi
+
+echo ""
+echo "Server started on $BASE_URL (PID: $SERVER_PID)"
+echo ""
+
+# Export BASE_URL for test scripts
+export BASE_URL
+
+# Run tests
+TOTAL_PASSED=0
+TOTAL_FAILED=0
+TESTS_RUN=0
+
+run_test() {
+ local test_file="$1"
+ local test_name=$(basename "$test_file" .sh)
+
+ echo "----------------------------------------"
+
+ if bash "$test_file"; then
+ ((TOTAL_PASSED++))
+ else
+ ((TOTAL_FAILED++))
+ fi
+ ((TESTS_RUN++))
+ echo ""
+}
+
+# Run all test files
+for test_file in "$SCRIPT_DIR/tests"/test_*.sh; do
+ if [ -f "$test_file" ]; then
+ run_test "$test_file"
+ fi
+done
+
+# Summary
+echo "========================================"
+echo "Test Summary"
+echo "========================================"
+echo "Total test files: $TESTS_RUN"
+echo -e "Passed: ${GREEN}$TOTAL_PASSED${NC}"
+echo -e "Failed: ${RED}$TOTAL_FAILED${NC}"
+echo ""
+
+if [ $TOTAL_FAILED -gt 0 ]; then
+ echo -e "${RED}SOME TESTS FAILED${NC}"
+ exit 1
+else
+ echo -e "${GREEN}ALL TESTS PASSED${NC}"
+ exit 0
+fi
diff --git a/vhosts/t/phpcon-kagawa-2025/tests/test_404.sh b/vhosts/t/phpcon-kagawa-2025/tests/test_404.sh
new file mode 100755
index 0000000..509dfdb
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/tests/test_404.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Test for 404 Not Found response
+
+BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
+PASSED=0
+FAILED=0
+
+echo "Testing 404 Not Found"
+echo "====================="
+
+# Test 1: GET / returns 404
+echo -n "Test 1: GET / returns 404... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/")
+if [ "$response" = "404" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 2: GET /nonexistent returns 404
+echo -n "Test 2: GET /nonexistent returns 404... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/nonexistent")
+if [ "$response" = "404" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 3: GET /foo/bar returns 404
+echo -n "Test 3: GET /foo/bar returns 404... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/foo/bar")
+if [ "$response" = "404" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 4: 404 response body contains "404 Not Found"
+echo -n "Test 4: 404 response body contains '404 Not Found'... "
+content=$(curl -s "$BASE_URL/nonexistent")
+if [ "$content" = "404 Not Found" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got '$content')"
+ ((FAILED++))
+fi
+
+echo ""
+echo "Results: $PASSED passed, $FAILED failed"
+
+if [ $FAILED -gt 0 ]; then
+ exit 1
+fi
+exit 0
diff --git a/vhosts/t/phpcon-kagawa-2025/tests/test_cookie.sh b/vhosts/t/phpcon-kagawa-2025/tests/test_cookie.sh
new file mode 100755
index 0000000..3678442
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/tests/test_cookie.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# Test for /cookie endpoint
+
+BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
+PASSED=0
+FAILED=0
+
+echo "Testing /cookie endpoint"
+echo "========================"
+
+# Test 1: GET /cookie returns 200
+echo -n "Test 1: GET /cookie returns 200... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/cookie")
+if [ "$response" = "200" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 2: GET /cookie returns udon shop page
+echo -n "Test 2: GET /cookie returns udon shop page... "
+content=$(curl -s "$BASE_URL/cookie")
+if echo "$content" | grep -q "うどん店"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 3: POST /cookie sets order cookie
+echo -n "Test 3: POST /cookie sets order cookie... "
+response=$(curl -s -i -X POST "$BASE_URL/cookie" \
+ -d "udon=%E3%81%8B%E3%81%91" | grep -i "Set-Cookie")
+if echo "$response" | grep -q "order="; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 4: GET /cookie with existing order shows order
+echo -n "Test 4: GET /cookie with existing order shows order... "
+content=$(curl -s -b "order=%E3%81%8B%E3%81%91" "$BASE_URL/cookie")
+if echo "$content" | grep -q "かけ"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 5: POST /cookie adds to existing orders
+echo -n "Test 5: POST /cookie adds to existing orders... "
+response=$(curl -s -i -X POST -b "order=%E3%81%8B%E3%81%91" "$BASE_URL/cookie" \
+ -d "udon=%E9%87%9C%E7%8E%89" | grep -i "Set-Cookie")
+if echo "$response" | grep -q "order="; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 6: Multiple orders are stored in cookie
+echo -n "Test 6: Multiple orders are displayed... "
+content=$(curl -s -b "order=%E3%81%8B%E3%81%91%2C%E9%87%9C%E7%8E%89" "$BASE_URL/cookie")
+if echo "$content" | grep -q "かけ" && echo "$content" | grep -q "釜玉"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+echo ""
+echo "Results: $PASSED passed, $FAILED failed"
+
+if [ $FAILED -gt 0 ]; then
+ exit 1
+fi
+exit 0
diff --git a/vhosts/t/phpcon-kagawa-2025/tests/test_cookie_eat.sh b/vhosts/t/phpcon-kagawa-2025/tests/test_cookie_eat.sh
new file mode 100755
index 0000000..d19229c
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/tests/test_cookie_eat.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+# Test for /cookie/eat endpoint
+
+BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
+PASSED=0
+FAILED=0
+
+echo "Testing /cookie/eat endpoint"
+echo "============================"
+
+# Test 1: GET /cookie/eat returns 200
+echo -n "Test 1: GET /cookie/eat returns 200... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/cookie/eat")
+if [ "$response" = "200" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 2: GET /cookie/eat without orders shows no order message
+echo -n "Test 2: GET /cookie/eat without orders shows message... "
+content=$(curl -s "$BASE_URL/cookie/eat")
+if echo "$content" | grep -q "注文がありません"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 3: GET /cookie/eat with orders shows orders
+echo -n "Test 3: GET /cookie/eat with orders shows orders... "
+content=$(curl -s -b "order=%E3%81%8B%E3%81%91" "$BASE_URL/cookie/eat")
+if echo "$content" | grep -q "かけ"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 4: GET /cookie/eat clears cookie
+echo -n "Test 4: GET /cookie/eat clears cookie... "
+response=$(curl -s -I -b "order=%E3%81%8B%E3%81%91" "$BASE_URL/cookie/eat" | grep -i "Set-Cookie")
+if echo "$response" | grep -q "Max-Age=0"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 5: GET /cookie/eat shows success message
+echo -n "Test 5: GET /cookie/eat shows success message... "
+content=$(curl -s -b "order=%E3%81%8B%E3%81%91" "$BASE_URL/cookie/eat")
+if echo "$content" | grep -q "ごちそうさまでした"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 6: GET /cookie/eat with multiple orders shows all
+echo -n "Test 6: GET /cookie/eat with multiple orders shows all... "
+content=$(curl -s -b "order=%E3%81%8B%E3%81%91%2C%E9%87%9C%E7%8E%89" "$BASE_URL/cookie/eat")
+if echo "$content" | grep -q "かけ" && echo "$content" | grep -q "釜玉"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+echo ""
+echo "Results: $PASSED passed, $FAILED failed"
+
+if [ $FAILED -gt 0 ]; then
+ exit 1
+fi
+exit 0
diff --git a/vhosts/t/phpcon-kagawa-2025/tests/test_get.sh b/vhosts/t/phpcon-kagawa-2025/tests/test_get.sh
new file mode 100755
index 0000000..99653b9
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/tests/test_get.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+# Test for /get endpoint
+
+BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
+PASSED=0
+FAILED=0
+
+echo "Testing /get endpoint"
+echo "====================="
+
+# Test 1: GET /get returns 200
+echo -n "Test 1: GET /get returns 200... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/get")
+if [ "$response" = "200" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 2: GET /get returns phpinfo
+echo -n "Test 2: GET /get returns phpinfo... "
+content=$(curl -s "$BASE_URL/get")
+if echo "$content" | grep -q "phpinfo()"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 3: GET /get contains PHP version
+echo -n "Test 3: GET /get contains PHP Version... "
+if echo "$content" | grep -q "PHP Version"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+echo ""
+echo "Results: $PASSED passed, $FAILED failed"
+
+if [ $FAILED -gt 0 ]; then
+ exit 1
+fi
+exit 0
diff --git a/vhosts/t/phpcon-kagawa-2025/tests/test_post.sh b/vhosts/t/phpcon-kagawa-2025/tests/test_post.sh
new file mode 100755
index 0000000..950c4ff
--- /dev/null
+++ b/vhosts/t/phpcon-kagawa-2025/tests/test_post.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+# Test for /post endpoint
+
+BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
+PASSED=0
+FAILED=0
+
+echo "Testing /post endpoint"
+echo "======================"
+
+# Test 1: GET /post returns 200
+echo -n "Test 1: GET /post returns 200... "
+response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/post")
+if [ "$response" = "200" ]; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED (got $response)"
+ ((FAILED++))
+fi
+
+# Test 2: GET /post returns quiz form
+echo -n "Test 2: GET /post returns quiz form... "
+content=$(curl -s "$BASE_URL/post")
+if echo "$content" | grep -q "本州四国連絡橋クイズ"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 3: POST /post with correct answers returns all correct
+echo -n "Test 3: POST /post with correct answers... "
+response=$(curl -s -X POST "$BASE_URL/post" \
+ -d "ehime=hiroshima&kagawa=okayama&tokushima=hyogo")
+if echo "$response" | grep -q "スコア: 3 / 3"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 4: POST /post with wrong answers returns score 0
+echo -n "Test 4: POST /post with wrong answers... "
+response=$(curl -s -X POST "$BASE_URL/post" \
+ -d "ehime=osaka&kagawa=osaka&tokushima=osaka")
+if echo "$response" | grep -q "スコア: 0 / 3"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+# Test 5: POST /post with partial correct answers
+echo -n "Test 5: POST /post with partial correct answers... "
+response=$(curl -s -X POST "$BASE_URL/post" \
+ -d "ehime=hiroshima&kagawa=osaka&tokushima=osaka")
+if echo "$response" | grep -q "スコア: 1 / 3"; then
+ echo "PASSED"
+ ((PASSED++))
+else
+ echo "FAILED"
+ ((FAILED++))
+fi
+
+echo ""
+echo "Results: $PASSED passed, $FAILED failed"
+
+if [ $FAILED -gt 0 ]; then
+ exit 1
+fi
+exit 0