From 619d3b673e9f7cb92978a048aa66a5451723e5c1 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Tue, 26 Dec 2023 12:17:57 +0100 Subject: [PATCH] Add `serve` command --- bin/serve | 7 + composer.json | 3 +- formwork/src/Commands/ServeCommand.php | 179 +++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 bin/serve create mode 100644 formwork/src/Commands/ServeCommand.php diff --git a/bin/serve b/bin/serve new file mode 100644 index 00000000..b7999ee6 --- /dev/null +++ b/bin/serve @@ -0,0 +1,7 @@ +start(); diff --git a/composer.json b/composer.json index 401aaaec..37ca77c5 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "scripts": { "fix": "php-cs-fixer fix --verbose", "phpstan": "phpstan analyse", - "phpstan:baseline": "phpstan analyse --generate-baseline" + "phpstan:baseline": "phpstan analyse --generate-baseline", + "serve": "php bin/serve" } } diff --git a/formwork/src/Commands/ServeCommand.php b/formwork/src/Commands/ServeCommand.php new file mode 100644 index 00000000..82c67f62 --- /dev/null +++ b/formwork/src/Commands/ServeCommand.php @@ -0,0 +1,179 @@ + + */ + protected array $requestData; + + protected Process $process; + + protected CLImate $climate; + + public function __construct(string $host = '127.0.0.1', int $port = 8000) + { + $this->host = $host; + $this->port = $port; + $this->climate = new CLImate(); + } + + public function start(): void + { + $php = (new PhpExecutableFinder())->find(); + + $this->process = new Process([ + $php, + '-S', + $this->host . ':' . $this->port, + 'formwork/server.php', + ], dirname(__DIR__, 3), null, null, 0); + + $this->process->run(function ($type, $buffer): void { + $this->handleOutput(explode("\n", $buffer)); + }); + } + + /** + * @param list $lines + */ + protected function handleOutput(array $lines): void + { + foreach ($lines as $line) { + if (!preg_match('/^\[(.+)\] (.+)$/', $line, $matches, PREG_UNMATCHED_AS_NULL)) { + continue; + } + + [, $date, $message] = $matches; + + if (!isset($date, $message)) { + continue; + } + + $date = (new DateTimeImmutable($date)); + + switch (true) { + case Str::contains($line, 'Development Server ('): + $this->climate->out(sprintf('Formwork %s Server running at http://%s:%d', App::VERSION, $this->host, $this->port)); + $this->climate->out('Press CTRL+C to stop'); + $this->climate->br(); + break; + + case Str::contains($line, 'Accepted'): + $acceptedTime = microtime(true); + + [, $requestPort, $requestInfo] = $this->splitMessage($message); + + $this->requestData[$requestPort] = ['time' => $acceptedTime]; + + break; + + case Str::contains($line, 'Closing'): + $closingTime = microtime(true); + + [, $requestPort, $requestInfo] = $this->splitMessage($message); + + preg_match( + '/^(?:\[(?\d{3})\]: (?[A-Z]+) (?[^ ]+)(?: -(? .+))?|(?.+))/', + $this->requestData[$requestPort]['info'], + $info, + PREG_UNMATCHED_AS_NULL + ); + + $this->climate->out(sprintf( + '%s %s ~%s', + $date->format('Y-m-d H:i:s'), + $info['method'] + ? sprintf('%s %s %s%s', $this->colorStatus((int) $info['status']), $info['method'], $info['uri'], $info['description']) + : $info['message'], + $this->formatTime($closingTime - $this->requestData[$requestPort]['time']) + )); + + break; + + case Str::contains($line, 'Failed to listen on'): + $this->process->stop(0); + + $this->climate->to('error')->out(sprintf('Formwork %s Server failed to listen on port %d', App::VERSION, $this->port)); + $this->climate->br(); + + $input = $this->climate->input('Enter another port:'); + $input->accept(fn ($response) => ctype_digit($response)); + + $this->port = (int) $input->prompt(); + + $this->climate->clear(); + + $this->start(); + + exit(1); + + default: + [, $requestPort, $requestInfo] = $this->splitMessage($message); + $this->requestData[$requestPort]['info'] = $requestInfo; + break; + } + } + } + + /** + * @return list + */ + protected function splitMessage(string $message): array + { + preg_match('/^([0-9.]+):(\d+) (.+)$/', $message, $matches, PREG_UNMATCHED_AS_NULL); + array_shift($matches); + return $matches; + } + + protected function colorStatus(int $status): string + { + if ($status <= 299) { + return "{$status}"; + } + if ($status <= 399) { + return "{$status}"; + } + if ($status <= 499) { + return "{$status}"; + } + if ($status <= 599) { + return "{$status}"; + } + + throw new UnexpectedValueException(); + } + + protected function formatTime(float $dt): string + { + if ($dt > 60) { + $m = floor($dt / 60); // minutes + $s = round($dt % 60); // seconds + return $m . ' m ' . $s . ' s'; + } + + if ($dt > 1) { + return round($dt, 1) . ' s'; // seconds + } + + if ($dt > 1e-3) { + return round($dt * 1e3) . ' ms'; // milliseconds + } + + return round($dt * 1e6) . ' μs'; // microseconds + } +}