diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-11-24 04:58:38 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-11-24 04:58:38 +0900 |
| commit | 67094790d2d9db5c99e7c136f49061a78698e57d (patch) | |
| tree | 02feb966e74c7c2d1b6a77d8310502aa9758649b | |
| parent | a071111365f9760b2f97fa3f6e12aee9f75dd15d (diff) | |
| download | nil.ninja-67094790d2d9db5c99e7c136f49061a78698e57d.tar.gz nil.ninja-67094790d2d9db5c99e7c136f49061a78698e57d.tar.zst nil.ninja-67094790d2d9db5c99e7c136f49061a78698e57d.zip | |
Add vhosts/t/phpcon-kagawa-2025/
30 files changed, 2211 insertions, 0 deletions
@@ -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 |
