From e15d7d8f8ba7103135d12309053fcfb7531f4744 Mon Sep 17 00:00:00 2001 From: ohmydevops Date: Sat, 23 May 2026 11:19:31 +0330 Subject: [PATCH 1/4] feat: remove unused content types and simplify request handling functions --- src/server_lib.php | 45 +++++--------------------------- tests/ServerFunctionsTest.php | 48 +++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/server_lib.php b/src/server_lib.php index a15bb92..d28066c 100644 --- a/src/server_lib.php +++ b/src/server_lib.php @@ -22,34 +22,6 @@ enum HTTP_STATUS: string case METHOD_NOT_ALLOWED = '405'; } -/** - * Default content type map for static files. - */ -const DEFAULT_CONTENT_TYPES = [ - 'html' => 'text/html;charset=utf-8', - 'css' => 'text/css', - 'js' => 'text/javascript', - 'apng' => 'image/apng', - 'gif' => 'image/gif', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'png' => 'image/png', - 'svg' => 'image/svg+xml', - 'webp' => 'image/webp', - 'ogg' => 'audio/ogg', - 'oga' => 'audio/ogg', - 'mp3' => 'audio/mpeg3', - 'wav' => 'audio/wav', - 'mp4' => 'video/mp4', - '.3gp' => 'video/3gpp', - 'flv' => 'video/x-flv', - 'mov' => 'video/quicktime', - 'mpg4' => 'video/mp4', - 'json' => 'application/json', - 'apk' => 'application/vnd.android.package-archive', -]; - - /** * Resolve the number of CPU cores for worker process sizing. */ @@ -103,7 +75,7 @@ function parse_request_context(string $request): array /** * Route request path to either static file response or error response. */ -function route_request_response(string $web_dir, string $request_path, array $accepted_encodings, array $content_types): array +function route_request_response(string $web_dir, string $request_path, array $accepted_encodings): array { if (str_contains($request_path, "\0")) { return handle_error_response(HTTP_STATUS::FORBIDDEN); @@ -123,8 +95,7 @@ function route_request_response(string $web_dir, string $request_path, array $ac return handle_error_response(HTTP_STATUS::NOT_FOUND); } - $extension = pathinfo($file_path, PATHINFO_EXTENSION); - $headers = [...DEFAULT_RESPONSE_HEADERS, 'Content-Type' => $content_types[$extension] ?? 'application/octet-stream']; + $headers = [...DEFAULT_RESPONSE_HEADERS, 'Content-Type' => mime_content_type($file_path) ?? 'application/octet-stream']; // Handle accepted encodings and static/on-the-fly gzip. if (in_array('gzip', $accepted_encodings, true)) { @@ -143,10 +114,10 @@ function route_request_response(string $web_dir, string $request_path, array $ac /** * Dispatch request handling by HTTP method (GET, HEAD, or 405). */ -function handle_request_by_method(string $web_dir, array $request_context, array $content_types): array +function handle_request_by_method(string $web_dir, array $request_context): array { $accepted_encodings = array_map('trim', explode(',', $request_context['headers']['accept-encoding'] ?? '')); - $resource_response = route_request_response($web_dir, $request_context['request_path'], $accepted_encodings, $content_types); + $resource_response = route_request_response($web_dir, $request_context['request_path'], $accepted_encodings); return match ($request_context['method']) { 'GET' => $resource_response, @@ -286,7 +257,6 @@ function read_request(\Socket $client): string|false function worker_process( \Socket $socket, string $web_dir, - array $content_types, int $keep_alive_max_requests, int $keep_alive_timeout ): void { @@ -295,7 +265,7 @@ function worker_process( socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $keep_alive_timeout, 'usec' => 0]); socket_set_option($client, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $keep_alive_timeout, 'usec' => 0]); - handle_client_connection($client, $web_dir, $content_types, $keep_alive_max_requests, $keep_alive_timeout); + handle_client_connection($client, $web_dir, $keep_alive_max_requests, $keep_alive_timeout); socket_close($client); } @@ -307,7 +277,6 @@ function worker_process( function handle_client_connection( \Socket $client, string $web_dir, - array $content_types, int $keep_alive_max_requests, int $keep_alive_timeout ): void { @@ -326,7 +295,7 @@ function handle_client_connection( $request_headers = $request_context['headers']; $first_line = $request_context['first_line']; - [$status_code, $headers, $body] = handle_request_by_method($web_dir, $request_context, $content_types); + [$status_code, $headers, $body] = handle_request_by_method($web_dir, $request_context); // Determine connection persistence (HTTP/1.1 defaults to keep-alive per RFC 7230) $client_wants_keepalive = ! isset($request_headers['connection']) @@ -389,7 +358,7 @@ function run_server(string $web_dir, ?int $worker_count): void $workers[] = $pid; } else { // Child process - become a worker - worker_process($sock, $web_dir, DEFAULT_CONTENT_TYPES, KEEP_ALIVE_MAX_REQUESTS, KEEP_ALIVE_TIMEOUT); + worker_process($sock, $web_dir, KEEP_ALIVE_MAX_REQUESTS, KEEP_ALIVE_TIMEOUT); exit(0); } } diff --git a/tests/ServerFunctionsTest.php b/tests/ServerFunctionsTest.php index 28081b5..0ded5ad 100644 --- a/tests/ServerFunctionsTest.php +++ b/tests/ServerFunctionsTest.php @@ -46,7 +46,6 @@ public function test_head_request_returns_empty_body_and_content_length(): void [$status, $headers, $body] = handle_request_by_method( dirname(__DIR__), $requestContext, - DEFAULT_CONTENT_TYPES ); $this->assertSame(HTTP_STATUS::OK, $status); @@ -61,7 +60,6 @@ public function test_path_traversal_returns_forbidden(): void dirname(__DIR__), '/../composer.json', [], - DEFAULT_CONTENT_TYPES ); $this->assertSame(HTTP_STATUS::FORBIDDEN, $status); @@ -80,7 +78,6 @@ public function test_gzip_response_sets_encoding_and_is_decodable(): void $tempDir, '/sample.txt', ['gzip'], - DEFAULT_CONTENT_TYPES ); $this->assertSame(HTTP_STATUS::OK, $status); @@ -106,14 +103,12 @@ public function test_gzip_response_body_is_smaller_than_plain_for_repetitive_con $tempDir, '/sample.txt', [], - DEFAULT_CONTENT_TYPES ); [, $gzipHeaders, $gzipBody] = route_request_response( $tempDir, '/sample.txt', ['gzip'], - DEFAULT_CONTENT_TYPES ); $this->assertSame('gzip', $gzipHeaders['Content-Encoding']); @@ -124,4 +119,47 @@ public function test_gzip_response_body_is_smaller_than_plain_for_repetitive_con rmdir($tempDir); } } + + public function test_content_type_is_set_based_on_file_extension(): void + { + $tempDir = sys_get_temp_dir() . '/joojoo-test-' . uniqid('', true); + mkdir($tempDir); + + $cases = [ + 'index.html' => [ + 'content' => 'tx', + 'assertion' => static fn (string $type): bool => str_starts_with($type, 'text/html'), + ], + 'style.css' => [ + 'content' => 'body { color: #111; font-family: sans-serif; }', + 'assertion' => static fn (string $type): bool => in_array($type, ['text/css', 'text/plain'], true), + ], + 'font.woff2' => [ + 'content' => "wOF2" . str_repeat("\0", 32), + 'assertion' => static fn (string $type): bool => in_array( + $type, + ['font/woff2', 'application/font-woff2', 'application/octet-stream'], + true + ), + ], + 'logo.png' => [ + 'content' => base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl3xQAAAABJRU5ErkJggg==', true), + 'assertion' => static fn (string $type): bool => $type === 'image/png', + ], + ]; + + try { + foreach ($cases as $filename => $case) { + file_put_contents("$tempDir/$filename", $case['content']); + + [, $headers] = route_request_response($tempDir, "/$filename", []); + + $this->assertTrue(($case['assertion'])($headers['Content-Type']), "Failed for $filename"); + + unlink("$tempDir/$filename"); + } + } finally { + rmdir($tempDir); + } + } } From ed4405dcbb24cd8f964970dd9dd72a48c9c51c61 Mon Sep 17 00:00:00 2001 From: ohmydevops Date: Sat, 23 May 2026 12:36:47 +0330 Subject: [PATCH 2/4] feat: add Cache-Control support with CLI and environment variable configuration --- docs/index.html | 24 +++++------ server.php | 3 +- src/cli.php | 33 ++++++++++++++- src/server_lib.php | 94 +++++++++++++++++++++++++++++++++++------ tests/CliConfigTest.php | 35 ++++++++++++++- 5 files changed, 158 insertions(+), 31 deletions(-) diff --git a/docs/index.html b/docs/index.html index 580a388..52b8f21 100644 --- a/docs/index.html +++ b/docs/index.html @@ -156,27 +156,23 @@

Request Flow

Quick Start

-

Local Usage

-
php server.php
-# or with custom directory
-BASE_WEB_DIR=/path/to/site php server.php
-# or set worker count
-WORKERS_COUNT=8 php server.php
-# or use CLI args
-php server.php --web-dir /path/to/site --workers-count 8
+
php server.php --web-dir /path/to/site

Configuration

-

Environment Variables:

-
    -
  • BASE_WEB_DIR — Root directory for serving files (default: current directory)
  • -
  • WORKERS_COUNT — Number of worker processes (default: CPU cores × 2)
  • -
-

CLI Arguments (take precedence over env vars):

+

CLI Arguments:

  • --web-dir PATH — Set root directory for serving files
  • --workers-count N — Set worker process count (must be >= 1)
  • +
  • --cache-enabled true|false — Enable or disable Cache-Control response header
  • +
+

Environment Variables:

+
    +
  • BASE_WEB_DIR — Environment variable for web root (used if --web-dir is not provided)
  • +
  • WORKERS_COUNT — Environment variable for worker count (used if --workers-count is not provided)
  • +
  • CACHE_ENABLED=true|false — Enable or disable Cache-Control response header
+

Precedence: CLI arguments override environment variables.

Docker

# Build
diff --git a/server.php b/server.php
index 5651d2e..6ed338d 100755
--- a/server.php
+++ b/server.php
@@ -10,5 +10,6 @@
 
 run_server(
     web_dir:$config['web_dir'],
-    worker_count:$config['worker_count']
+    worker_count:$config['worker_count'],
+    cache_enabled:$config['cache_enabled']
 );
diff --git a/src/cli.php b/src/cli.php
index 8089654..27f1854 100644
--- a/src/cli.php
+++ b/src/cli.php
@@ -8,16 +8,19 @@
  * Supports:
  * - --web-dir PATH: Set the web root directory
  * - --workers-count N: Set worker process count (must be >= 1)
+ * - --cache-enabled true|false: Enable or disable Cache-Control response header
+ * - --no-cache: Alias for --cache-enabled false
  *
  * @param array $argv Command-line arguments
  *
- * @return array{web_dir: string|null, workers_count: int|null}
+ * @return array{web_dir: string|null, workers_count: int|null, cache_enabled: bool|null}
  */
 function parse_cli_arguments(array $argv): array
 {
     $config = [
         'web_dir' => null,
         'workers_count' => null,
+        'cache_enabled' => null,
     ];
 
     foreach ($argv as $i => $arg) {
@@ -33,6 +36,10 @@ function parse_cli_arguments(array $argv): array
             }
             $config['workers_count'] = $workers;
         }
+
+        if ($arg === '--cache-enabled' && isset($argv[$i + 1])) {
+            $config['cache_enabled'] = normalize_boolean($argv[$i + 1]);
+        }
     }
 
     return $config;
@@ -45,11 +52,12 @@ function parse_cli_arguments(array $argv): array
  * Supported environment variables:
  * - BASE_WEB_DIR: Root directory for serving files
  * - WORKERS_COUNT: Number of worker processes
+ * - CACHE_ENABLED: Enable/disable Cache-Control response header
  *
  * @param array $argv Command-line arguments
  * @param string|null $default_web_dir Default web directory if neither env nor CLI provides one
  *
- * @return array{web_dir: string, worker_count: int|null}
+ * @return array{web_dir: string, worker_count: int|null, cache_enabled: bool}
  */
 function load_config(array $argv, ?string $default_web_dir = null): array
 {
@@ -75,8 +83,29 @@ function load_config(array $argv, ?string $default_web_dir = null): array
         $worker_count = $cli_config['workers_count'];
     }
 
+    // Cache-Control switch: env -> CLI (CLI takes precedence)
+    $cache_enabled = true;
+    $cache_enabled_env = $_ENV['CACHE_ENABLED'] ?? $_SERVER['CACHE_ENABLED'] ?? null;
+    if ($cache_enabled_env !== null) {
+        $cache_enabled = normalize_boolean($cache_enabled_env);
+    }
+    if ($cli_config['cache_enabled'] !== null) {
+        $cache_enabled = $cli_config['cache_enabled'];
+    }
+
     return [
         'web_dir' => $web_dir,
         'worker_count' => $worker_count,
+        'cache_enabled' => $cache_enabled,
     ];
 }
+
+/**
+ * Normalize common truthy values from env/CLI string inputs.
+ */
+function normalize_boolean(mixed $value): bool
+{
+    $normalized = strtolower(trim((string) $value));
+
+    return in_array($normalized, ['1', 'true', 'ture', 'yes', 'on'], true);
+}
diff --git a/src/server_lib.php b/src/server_lib.php
index d28066c..da8581f 100644
--- a/src/server_lib.php
+++ b/src/server_lib.php
@@ -41,6 +41,18 @@ function logging(string $message): void
     echo $message . PHP_EOL;
 }
 
+function cache_control_header(string $extension): string
+{
+    $ext = strtolower($extension);
+    $static = ['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'woff', 'woff2', 'ttf', 'webp'];
+
+    return match (true) {
+        $ext === 'html' => 'no-cache',
+        in_array($ext, $static, true) => 'public, max-age=86400',
+        default => 'no-cache',
+    };
+}
+
 /**
  * Parse request metadata (method, path, headers, first line) from a raw request.
  */
@@ -72,10 +84,57 @@ function parse_request_context(string $request): array
     ];
 }
 
+function content_type(string $file_path): string
+{
+    static $finfo = null;
+    static $extension_map = [
+        'css' => 'text/css',
+        'js' => 'text/javascript',
+        'html' => 'text/html; charset=utf-8',
+        'htm' => 'text/html; charset=utf-8',
+        'json' => 'application/json',
+        'svg' => 'image/svg+xml',
+        'woff' => 'font/woff',
+        'woff2' => 'font/woff2',
+        'ttf' => 'font/ttf',
+        'png' => 'image/png',
+        'jpg' => 'image/jpeg',
+        'jpeg' => 'image/jpeg',
+        'gif' => 'image/gif',
+        'ico' => 'image/x-icon',
+        'pdf' => 'application/pdf',
+        'txt' => 'text/plain',
+        'webp' => 'image/webp',
+        'zip' => 'application/zip',
+        'gz' => 'application/gzip',
+        'mp4' => 'video/mp4',
+        'mp3' => 'audio/mpeg',
+        'mkv' => 'video/x-matroska',
+    ];
+
+    $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
+    if ($ext !== '' && isset($extension_map[$ext])) {
+        return $extension_map[$ext];
+    }
+
+    if ($finfo === null) {
+        $finfo = finfo_open(FILEINFO_MIME_TYPE) ?: false;
+    }
+
+    if ($finfo !== false) {
+        $type = finfo_file($finfo, $file_path);
+        if (is_string($type) && $type !== '') {
+            return $type;
+        }
+    }
+
+    return 'application/octet-stream';
+}
+
 /**
  * Route request path to either static file response or error response.
  */
-function route_request_response(string $web_dir, string $request_path, array $accepted_encodings): array
+function route_request_response(string $web_dir, string $request_path, array $accepted_encodings, bool $cache_enabled = true): array
 {
     if (str_contains($request_path, "\0")) {
         return handle_error_response(HTTP_STATUS::FORBIDDEN);
@@ -95,7 +154,7 @@ function route_request_response(string $web_dir, string $request_path, array $ac
         return handle_error_response(HTTP_STATUS::NOT_FOUND);
     }
 
-    $headers = [...DEFAULT_RESPONSE_HEADERS, 'Content-Type' => mime_content_type($file_path) ?? 'application/octet-stream'];
+    $headers = [...DEFAULT_RESPONSE_HEADERS, 'Content-Type' => content_type($file_path)];
 
     // Handle accepted encodings and static/on-the-fly gzip.
     if (in_array('gzip', $accepted_encodings, true)) {
@@ -108,16 +167,21 @@ function route_request_response(string $web_dir, string $request_path, array $ac
         $body = file_get_contents($file_path);
     }
 
+    // Handle Cache-Control for static assets unless disabled by config.
+    if ($cache_enabled) {
+        $headers['Cache-Control'] = cache_control_header(pathinfo($file_path, PATHINFO_EXTENSION));
+    }
+
     return [HTTP_STATUS::OK, $headers, $body];
 }
 
 /**
  * Dispatch request handling by HTTP method (GET, HEAD, or 405).
  */
-function handle_request_by_method(string $web_dir, array $request_context): array
+function handle_request_by_method(string $web_dir, array $request_context, bool $cache_enabled = true): array
 {
     $accepted_encodings = array_map('trim', explode(',', $request_context['headers']['accept-encoding'] ?? ''));
-    $resource_response = route_request_response($web_dir, $request_context['request_path'], $accepted_encodings);
+    $resource_response = route_request_response($web_dir, $request_context['request_path'], $accepted_encodings, $cache_enabled);
 
     return match ($request_context['method']) {
         'GET' => $resource_response,
@@ -258,14 +322,15 @@ function worker_process(
     \Socket $socket,
     string $web_dir,
     int $keep_alive_max_requests,
-    int $keep_alive_timeout
+    int $keep_alive_timeout,
+    bool $cache_enabled
 ): void {
     while ($client = socket_accept($socket)) {
         // Configure timeouts for keep-alive
         socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $keep_alive_timeout, 'usec' => 0]);
         socket_set_option($client, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $keep_alive_timeout, 'usec' => 0]);
 
-        handle_client_connection($client, $web_dir, $keep_alive_max_requests, $keep_alive_timeout);
+        handle_client_connection($client, $web_dir, $keep_alive_max_requests, $keep_alive_timeout, $cache_enabled);
 
         socket_close($client);
     }
@@ -278,7 +343,8 @@ function handle_client_connection(
     \Socket $client,
     string $web_dir,
     int $keep_alive_max_requests,
-    int $keep_alive_timeout
+    int $keep_alive_timeout,
+    bool $cache_enabled
 ): void {
     $request_count = 0;
     $keep_connection = true;
@@ -295,7 +361,7 @@ function handle_client_connection(
         $request_headers = $request_context['headers'];
         $first_line = $request_context['first_line'];
 
-        [$status_code, $headers, $body] = handle_request_by_method($web_dir, $request_context);
+        [$status_code, $headers, $body] = handle_request_by_method($web_dir, $request_context, $cache_enabled);
 
         // Determine connection persistence (HTTP/1.1 defaults to keep-alive per RFC 7230)
         $client_wants_keepalive = ! isset($request_headers['connection'])
@@ -334,7 +400,7 @@ function handle_client_connection(
 /**
  * Start the prefork HTTP server.
  */
-function run_server(string $web_dir, ?int $worker_count): void
+function run_server(string $web_dir, ?int $worker_count, bool $cache_enabled = true): void
 {
     $workers = [];
     $sock = create_server_socket(HOST, PORT);
@@ -358,19 +424,21 @@ function run_server(string $web_dir, ?int $worker_count): void
             $workers[] = $pid;
         } else {
             // Child process - become a worker
-            worker_process($sock, $web_dir, KEEP_ALIVE_MAX_REQUESTS, KEEP_ALIVE_TIMEOUT);
+            worker_process($sock, $web_dir, KEEP_ALIVE_MAX_REQUESTS, KEEP_ALIVE_TIMEOUT, $cache_enabled);
             exit(0);
         }
     }
     logging('');
-    logging("\033[92m Server is running on " . HOST . ':' . PORT . ' with ' . $worker_count . ' workers. ' . "\033[0m");
-    logging("\033[92m Serving files from: " . $web_dir . "\033[0m");
-
+    logging("\033[92m Server is running on: \033[0mhttp://" . HOST . ':' . PORT);
+    logging("\033[92m Worker processes: \033[0m" . count($workers));
+    logging("\033[92m Cache headers: \033[0m" . ($cache_enabled ? 'enabled' : 'disabled'));
+    logging("\033[92m Serving files from: \033[0m" . $web_dir);
     if (! is_dir($web_dir)) {
         logging('Warning: directory does not exist');
     } elseif (! is_readable($web_dir)) {
         logging('Warning: directory is not readable');
     }
+    logging(" Press Ctrl+C to stop the server");
 
     // Wait for all workers
     foreach ($workers as $worker_pid) {
diff --git a/tests/CliConfigTest.php b/tests/CliConfigTest.php
index 673058e..ceda387 100644
--- a/tests/CliConfigTest.php
+++ b/tests/CliConfigTest.php
@@ -15,6 +15,7 @@ public function test_parse_cli_arguments_no_args(): void
 
         $this->assertNull($config['web_dir']);
         $this->assertNull($config['workers_count']);
+        $this->assertNull($config['cache_enabled']);
     }
 
     public function test_parse_cli_arguments_web_dir(): void
@@ -24,6 +25,7 @@ public function test_parse_cli_arguments_web_dir(): void
 
         $this->assertSame('/path/to/site', $config['web_dir']);
         $this->assertNull($config['workers_count']);
+        $this->assertNull($config['cache_enabled']);
     }
 
     public function test_parse_cli_arguments_workers_count(): void
@@ -33,6 +35,15 @@ public function test_parse_cli_arguments_workers_count(): void
 
         $this->assertNull($config['web_dir']);
         $this->assertSame(8, $config['workers_count']);
+        $this->assertNull($config['cache_enabled']);
+    }
+
+    public function test_parse_cli_arguments_cache_enabled_false(): void
+    {
+        $argv = ['server.php', '--cache-enabled', 'false'];
+        $config = parse_cli_arguments($argv);
+
+        $this->assertFalse($config['cache_enabled']);
     }
 
     public function test_parse_cli_arguments_both_options(): void
@@ -66,6 +77,7 @@ public function test_load_config_defaults(): void
 
         $this->assertSame($default_dir, $config['web_dir']);
         $this->assertNull($config['worker_count']);
+        $this->assertTrue($config['cache_enabled']);
     }
 
     public function test_load_config_cli_args_override_defaults(): void
@@ -77,12 +89,14 @@ public function test_load_config_cli_args_override_defaults(): void
 
         $this->assertSame('docs', $config['web_dir']);
         $this->assertSame(6, $config['worker_count']);
+        $this->assertTrue($config['cache_enabled']);
     }
 
     public function test_load_config_env_var_precedence(): void
     {
         $_ENV['BASE_WEB_DIR'] = '/env/web';
         $_ENV['WORKERS_COUNT'] = 12;
+        $_ENV['CACHE_ENABLED'] = 'false';
 
         $argv = ['server.php'];
 
@@ -90,33 +104,39 @@ public function test_load_config_env_var_precedence(): void
 
         $this->assertSame('/env/web', $config['web_dir']);
         $this->assertSame(12, $config['worker_count']);
+        $this->assertFalse($config['cache_enabled']);
 
         // Cleanup
         unset($_ENV['BASE_WEB_DIR']);
         unset($_ENV['WORKERS_COUNT']);
+        unset($_ENV['CACHE_ENABLED']);
     }
 
     public function test_load_config_cli_overrides_env(): void
     {
         $_ENV['BASE_WEB_DIR'] = '/env/web';
         $_ENV['WORKERS_COUNT'] = 12;
+        $_ENV['CACHE_ENABLED'] = 'true';
 
-        $argv = ['server.php', '--web-dir', '/cli/web', '--workers-count', '4'];
+        $argv = ['server.php', '--web-dir', '/cli/web', '--workers-count', '4', '--cache-enabled', 'false'];
 
         $config = load_config($argv, '/default');
 
         $this->assertSame('/cli/web', $config['web_dir']);
         $this->assertSame(4, $config['worker_count']);
+        $this->assertFalse($config['cache_enabled']);
 
         // Cleanup
         unset($_ENV['BASE_WEB_DIR']);
         unset($_ENV['WORKERS_COUNT']);
+        unset($_ENV['CACHE_ENABLED']);
     }
 
     public function test_load_config_cli_overrides_env_partially(): void
     {
         $_ENV['BASE_WEB_DIR'] = '/env/web';
         $_ENV['WORKERS_COUNT'] = 12;
+        $_ENV['CACHE_ENABLED'] = 'false';
 
         $argv = ['server.php', '--workers-count', '3'];
 
@@ -124,9 +144,22 @@ public function test_load_config_cli_overrides_env_partially(): void
 
         $this->assertSame('/env/web', $config['web_dir']);
         $this->assertSame(3, $config['worker_count']);
+        $this->assertFalse($config['cache_enabled']);
 
         // Cleanup
         unset($_ENV['BASE_WEB_DIR']);
         unset($_ENV['WORKERS_COUNT']);
+        unset($_ENV['CACHE_ENABLED']);
+    }
+
+    public function test_load_config_accepts_ture_for_cache_enabled_env(): void
+    {
+        $_ENV['CACHE_ENABLED'] = 'ture';
+
+        $config = load_config(['server.php'], '/default');
+
+        $this->assertTrue($config['cache_enabled']);
+
+        unset($_ENV['CACHE_ENABLED']);
     }
 }

From 77cfd8efefbbc74f182b41f0965a3ed43ff85cba Mon Sep 17 00:00:00 2001
From: ohmydevops 
Date: Sat, 23 May 2026 12:42:02 +0330
Subject: [PATCH 3/4] feat: enhance CLI with help option and update
 documentation

---
 docs/index.html         |  1 +
 server.php              |  5 +++++
 src/cli.php             | 30 +++++++++++++++++++++++++++++-
 tests/CliConfigTest.php | 28 ++++++++++++++++++++++++++++
 4 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/docs/index.html b/docs/index.html
index 52b8f21..5c67b31 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -162,6 +162,7 @@ 

Local Usage

Configuration

CLI Arguments:

    +
  • -h, --help — Show CLI usage help and exit
  • --web-dir PATH — Set root directory for serving files
  • --workers-count N — Set worker process count (must be >= 1)
  • --cache-enabled true|false — Enable or disable Cache-Control response header
  • diff --git a/server.php b/server.php index 6ed338d..819fc90 100755 --- a/server.php +++ b/server.php @@ -8,6 +8,11 @@ $config = load_config($argv, __DIR__); +if($config['show_help']) { + echo cli_help_text(); + exit(0); +} + run_server( web_dir:$config['web_dir'], worker_count:$config['worker_count'], diff --git a/src/cli.php b/src/cli.php index 27f1854..f7970c5 100644 --- a/src/cli.php +++ b/src/cli.php @@ -6,6 +6,7 @@ * Parse CLI arguments into a configuration array. * * Supports: + * - -h, --help: Show usage help and exit * - --web-dir PATH: Set the web root directory * - --workers-count N: Set worker process count (must be >= 1) * - --cache-enabled true|false: Enable or disable Cache-Control response header @@ -13,7 +14,7 @@ * * @param array $argv Command-line arguments * - * @return array{web_dir: string|null, workers_count: int|null, cache_enabled: bool|null} + * @return array{web_dir: string|null, workers_count: int|null, cache_enabled: bool|null, show_help: bool} */ function parse_cli_arguments(array $argv): array { @@ -21,9 +22,14 @@ function parse_cli_arguments(array $argv): array 'web_dir' => null, 'workers_count' => null, 'cache_enabled' => null, + 'show_help' => false, ]; foreach ($argv as $i => $arg) { + if ($arg === '--help' || $arg === '-h') { + $config['show_help'] = true; + } + if ($arg === '--web-dir' && isset($argv[$i + 1])) { $config['web_dir'] = $argv[$i + 1]; } @@ -45,6 +51,27 @@ function parse_cli_arguments(array $argv): array return $config; } +/** + * Build CLI usage text. + */ +function cli_help_text(): string +{ + return "Usage: php server.php [options]\n" + . "\n" + . "Options:\n" + . " -h, --help Show this help and exit\n" + . " --web-dir PATH Set root directory for serving files\n" + . " --workers-count N Set worker process count (must be >= 1)\n" + . " --cache-enabled true|false Enable or disable Cache-Control header\n" + . "\n" + . "Environment variables:\n" + . " BASE_WEB_DIR Root directory for serving files\n" + . " WORKERS_COUNT Worker process count\n" + . " CACHE_ENABLED=true|false Enable or disable Cache-Control header\n" + . "\n" + . "Precedence: CLI arguments override environment variables.\n"; +} + /** * Merge environment variables and CLI arguments into final configuration. * CLI arguments take precedence over environment variables. @@ -97,6 +124,7 @@ function load_config(array $argv, ?string $default_web_dir = null): array 'web_dir' => $web_dir, 'worker_count' => $worker_count, 'cache_enabled' => $cache_enabled, + 'show_help' => $cli_config['show_help'], ]; } diff --git a/tests/CliConfigTest.php b/tests/CliConfigTest.php index ceda387..153195a 100644 --- a/tests/CliConfigTest.php +++ b/tests/CliConfigTest.php @@ -16,6 +16,23 @@ public function test_parse_cli_arguments_no_args(): void $this->assertNull($config['web_dir']); $this->assertNull($config['workers_count']); $this->assertNull($config['cache_enabled']); + $this->assertFalse($config['show_help']); + } + + public function test_parse_cli_arguments_help_long_flag(): void + { + $argv = ['server.php', '--help']; + $config = parse_cli_arguments($argv); + + $this->assertTrue($config['show_help']); + } + + public function test_parse_cli_arguments_help_short_flag(): void + { + $argv = ['server.php', '-h']; + $config = parse_cli_arguments($argv); + + $this->assertTrue($config['show_help']); } public function test_parse_cli_arguments_web_dir(): void @@ -162,4 +179,15 @@ public function test_load_config_accepts_ture_for_cache_enabled_env(): void unset($_ENV['CACHE_ENABLED']); } + + public function test_cli_help_text_contains_main_options(): void + { + $help = cli_help_text(); + + $this->assertStringContainsString('--help', $help); + $this->assertStringContainsString('--web-dir', $help); + $this->assertStringContainsString('--workers-count', $help); + $this->assertStringContainsString('--cache-enabled', $help); + $this->assertStringContainsString('CACHE_ENABLED', $help); + } } From 7b190f6098656b921230379d9769498ee21c5d7f Mon Sep 17 00:00:00 2001 From: ohmydevops Date: Sat, 23 May 2026 12:44:20 +0330 Subject: [PATCH 4/4] fix: standardize string quotes in server and test files --- server.php | 2 +- src/server_lib.php | 2 +- tests/ServerFunctionsTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server.php b/server.php index 819fc90..3644be3 100755 --- a/server.php +++ b/server.php @@ -8,7 +8,7 @@ $config = load_config($argv, __DIR__); -if($config['show_help']) { +if ($config['show_help']) { echo cli_help_text(); exit(0); } diff --git a/src/server_lib.php b/src/server_lib.php index da8581f..3b55321 100644 --- a/src/server_lib.php +++ b/src/server_lib.php @@ -438,7 +438,7 @@ function run_server(string $web_dir, ?int $worker_count, bool $cache_enabled = t } elseif (! is_readable($web_dir)) { logging('Warning: directory is not readable'); } - logging(" Press Ctrl+C to stop the server"); + logging(' Press Ctrl+C to stop the server'); // Wait for all workers foreach ($workers as $worker_pid) { diff --git a/tests/ServerFunctionsTest.php b/tests/ServerFunctionsTest.php index 0ded5ad..8774946 100644 --- a/tests/ServerFunctionsTest.php +++ b/tests/ServerFunctionsTest.php @@ -135,7 +135,7 @@ public function test_content_type_is_set_based_on_file_extension(): void 'assertion' => static fn (string $type): bool => in_array($type, ['text/css', 'text/plain'], true), ], 'font.woff2' => [ - 'content' => "wOF2" . str_repeat("\0", 32), + 'content' => 'wOF2' . str_repeat("\0", 32), 'assertion' => static fn (string $type): bool => in_array( $type, ['font/woff2', 'application/font-woff2', 'application/octet-stream'],