Skip to content
79 changes: 73 additions & 6 deletions src/Illuminate/Foundation/Console/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
namespace Illuminate\Foundation\Console;

use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Env;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use function Termwind\terminal;

#[AsCommand(name: 'serve')]
class ServeCommand extends Command
Expand Down Expand Up @@ -44,6 +46,13 @@ class ServeCommand extends Command
*/
protected $portOffset = 0;

/**
* The list of requests being handled and their start time.
*
* @var array<int, \Illuminate\Support\Carbon>
*/
protected $requestsPool;

/**
* The environment variables that should be passed from host machine to the PHP server process.
*
Expand All @@ -69,8 +78,6 @@ class ServeCommand extends Command
*/
public function handle()
{
$this->line("<info>Starting Laravel development server:</info> http://{$this->host()}:{$this->port()}");

$environmentFile = $this->option('env')
? base_path('.env').'.'.$this->option('env')
: base_path('.env');
Expand All @@ -93,7 +100,9 @@ public function handle()
filemtime($environmentFile) > $environmentLastModified) {
$environmentLastModified = filemtime($environmentFile);

$this->comment('Environment modified. Restarting server...');
$this->newLine();

$this->components->info('Environment modified. Restarting server...');

$process->stop(5);

Expand Down Expand Up @@ -130,9 +139,7 @@ protected function startProcess($hasEnvironment)
return in_array($key, static::$passthroughVariables) ? [$key => $value] : [$key => false];
})->all());

$process->start(function ($type, $buffer) {
$this->output->write($buffer);
});
$process->start($this->handleProcessOutput());

return $process;
}
Expand Down Expand Up @@ -212,6 +219,66 @@ protected function canTryAnotherPort()
($this->input->getOption('tries') > $this->portOffset);
}

/**
* Returns a "callable" to handle the process output.
*
* @return callable(string, string): void
*/
protected function handleProcessOutput()
{
return fn ($type, $buffer) => str($buffer)->explode(PHP_EOL)->each(function ($line) {
$parts = explode(']', $line);

if (str($line)->contains('Development Server (http')) {
$this->components->info("Server running on [http://{$this->host()}:{$this->port()}].");
$this->comment(' <fg=yellow;options=bold>Press Ctrl+C to stop the server</>');

$this->newLine();
} elseif (str($line)->contains(' Accepted')) {
$startDate = Carbon::createFromFormat('D M d H:i:s Y', ltrim($parts[0], '['));

preg_match('/\:(\d+)/', $parts[1], $matches);

$this->requestsPool[$matches[1]] = [$startDate, false];
} elseif (str($line)->contains([' [200]: GET '])) {
preg_match('/\:(\d+)/', $parts[1], $matches);

$this->requestsPool[$matches[1]][1] = trim(explode('[200]: GET', $line)[1]);
} elseif (str($line)->contains(' Closing')) {
preg_match('/\:(\d+)/', $parts[1], $matches);

$request = $this->requestsPool[$matches[1]];

[$startDate, $file] = $request;
$formattedStartedAt = $startDate->format('Y-m-d H:i:s');

unset($this->requestsPool[$matches[1]]);

[$date, $time] = explode(' ', $formattedStartedAt);

$this->output->write(" <fg=gray>$date</> $time");

$runTime = Carbon::createFromFormat('D M d H:i:s Y', ltrim($parts[0], '['))
->diffInSeconds($startDate);

if ($file) {
$this->output->write($file = " $file");
}

$dots = max(terminal()->width() - mb_strlen($formattedStartedAt) - mb_strlen($file) - mb_strlen($runTime) - 9, 0);

$this->output->write(' '.str_repeat('<fg=gray>.</>', $dots));
$this->output->writeln(" <fg=gray>~ {$runTime}s</>");
} elseif (str($line)->contains(['Closed without sending a request', ']: '])) {
// ...
} elseif (isset($parts[1])) {
$this->components->warn($parts[1]);
} elseif (! empty($line)) {
$this->components->warn($line);
}
});
}

/**
* Get the console command options.
*
Expand Down