Skip to content

Commit 75d9e6c

Browse files
Restored serve command
Use: `php artisan serve` to provide a temporary development server
1 parent 998af7a commit 75d9e6c

File tree

1 file changed

+340
-0
lines changed

1 file changed

+340
-0
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
<?php
2+
3+
namespace Illuminate\Foundation\Console;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Carbon;
7+
use Illuminate\Support\Env;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
use Symfony\Component\Console\Input\InputOption;
10+
use Symfony\Component\Process\PhpExecutableFinder;
11+
use Symfony\Component\Process\Process;
12+
13+
use function Termwind\terminal;
14+
15+
#[AsCommand(name: 'serve')]
16+
class ServeCommand extends Command
17+
{
18+
/**
19+
* The console command name.
20+
*
21+
* @var string
22+
*/
23+
protected $name = 'serve';
24+
25+
/**
26+
* The name of the console command.
27+
*
28+
* This name is used to identify the command during lazy loading.
29+
*
30+
* @var string|null
31+
*
32+
* @deprecated
33+
*/
34+
protected static $defaultName = 'serve';
35+
36+
/**
37+
* The console command description.
38+
*
39+
* @var string
40+
*/
41+
protected $description = 'Serve the application on the PHP development server';
42+
43+
/**
44+
* The current port offset.
45+
*
46+
* @var int
47+
*/
48+
protected $portOffset = 0;
49+
50+
/**
51+
* The list of requests being handled and their start time.
52+
*
53+
* @var array<int, \Illuminate\Support\Carbon>
54+
*/
55+
protected $requestsPool;
56+
57+
/**
58+
* Indicates if the "Server running on..." output message has been displayed.
59+
*
60+
* @var bool
61+
*/
62+
protected $serverRunningHasBeenDisplayed = false;
63+
64+
/**
65+
* The environment variables that should be passed from host machine to the PHP server process.
66+
*
67+
* @var string[]
68+
*/
69+
public static $passthroughVariables = [
70+
'APP_ENV',
71+
'LARAVEL_SAIL',
72+
'PATH',
73+
'PHP_CLI_SERVER_WORKERS',
74+
'PHP_IDE_CONFIG',
75+
'SYSTEMROOT',
76+
'XDEBUG_CONFIG',
77+
'XDEBUG_MODE',
78+
'XDEBUG_SESSION',
79+
];
80+
81+
/**
82+
* Execute the console command.
83+
*
84+
* @return int
85+
*
86+
* @throws \Exception
87+
*/
88+
public function handle()
89+
{
90+
$environmentFile = $this->option('env')
91+
? base_path('.env').'.'.$this->option('env')
92+
: base_path('.env');
93+
94+
$hasEnvironment = file_exists($environmentFile);
95+
96+
$environmentLastModified = $hasEnvironment
97+
? filemtime($environmentFile)
98+
: now()->addDays(30)->getTimestamp();
99+
100+
$process = $this->startProcess($hasEnvironment);
101+
102+
while ($process->isRunning()) {
103+
if ($hasEnvironment) {
104+
clearstatcache(false, $environmentFile);
105+
}
106+
107+
if (! $this->option('no-reload') &&
108+
$hasEnvironment &&
109+
filemtime($environmentFile) > $environmentLastModified) {
110+
$environmentLastModified = filemtime($environmentFile);
111+
112+
$this->newLine();
113+
114+
$this->components->info('Environment modified. Restarting server...');
115+
116+
$process->stop(5);
117+
118+
$this->serverRunningHasBeenDisplayed = false;
119+
120+
$process = $this->startProcess($hasEnvironment);
121+
}
122+
123+
usleep(500 * 1000);
124+
}
125+
126+
$status = $process->getExitCode();
127+
128+
if ($status && $this->canTryAnotherPort()) {
129+
$this->portOffset += 1;
130+
131+
return $this->handle();
132+
}
133+
134+
return $status;
135+
}
136+
137+
/**
138+
* Start a new server process.
139+
*
140+
* @param bool $hasEnvironment
141+
* @return \Symfony\Component\Process\Process
142+
*/
143+
protected function startProcess($hasEnvironment)
144+
{
145+
$process = new Process($this->serverCommand(), base_path(), collect($_ENV)->mapWithKeys(function ($value, $key) use ($hasEnvironment) {
146+
if ($this->option('no-reload') || ! $hasEnvironment) {
147+
return [$key => $value];
148+
}
149+
150+
return in_array($key, static::$passthroughVariables) ? [$key => $value] : [$key => false];
151+
})->all());
152+
153+
$process->start($this->handleProcessOutput());
154+
155+
return $process;
156+
}
157+
158+
/**
159+
* Get the full server command.
160+
*
161+
* @return array
162+
*/
163+
protected function serverCommand()
164+
{
165+
$server = file_exists(base_path('server.php'))
166+
? base_path('server.php')
167+
: __DIR__.'/../resources/server.php';
168+
169+
return [
170+
(new PhpExecutableFinder)->find(false),
171+
'-S',
172+
$this->host().':'.$this->port(),
173+
$server,
174+
];
175+
}
176+
177+
/**
178+
* Get the host for the command.
179+
*
180+
* @return string
181+
*/
182+
protected function host()
183+
{
184+
[$host] = $this->getHostAndPort();
185+
186+
return $host;
187+
}
188+
189+
/**
190+
* Get the port for the command.
191+
*
192+
* @return string
193+
*/
194+
protected function port()
195+
{
196+
$port = $this->input->getOption('port');
197+
198+
if (is_null($port)) {
199+
[, $port] = $this->getHostAndPort();
200+
}
201+
202+
$port = $port ?: 8000;
203+
204+
return $port + $this->portOffset;
205+
}
206+
207+
/**
208+
* Get the host and port from the host option string.
209+
*
210+
* @return array
211+
*/
212+
protected function getHostAndPort()
213+
{
214+
$hostParts = explode(':', $this->input->getOption('host'));
215+
216+
return [
217+
$hostParts[0],
218+
$hostParts[1] ?? null,
219+
];
220+
}
221+
222+
/**
223+
* Check if the command has reached its maximum number of port tries.
224+
*
225+
* @return bool
226+
*/
227+
protected function canTryAnotherPort()
228+
{
229+
return is_null($this->input->getOption('port')) &&
230+
($this->input->getOption('tries') > $this->portOffset);
231+
}
232+
233+
/**
234+
* Returns a "callable" to handle the process output.
235+
*
236+
* @return callable(string, string): void
237+
*/
238+
protected function handleProcessOutput()
239+
{
240+
return fn ($type, $buffer) => str($buffer)->explode("\n")->each(function ($line) {
241+
if (str($line)->contains('Development Server (http')) {
242+
if ($this->serverRunningHasBeenDisplayed) {
243+
return;
244+
}
245+
246+
$this->components->info("Server running on [http://{$this->host()}:{$this->port()}].");
247+
$this->comment(' <fg=yellow;options=bold>Press Ctrl+C to stop the server</>');
248+
249+
$this->newLine();
250+
251+
$this->serverRunningHasBeenDisplayed = true;
252+
} elseif (str($line)->contains(' Accepted')) {
253+
$requestPort = $this->getRequestPortFromLine($line);
254+
255+
$this->requestsPool[$requestPort] = [
256+
$this->getDateFromLine($line),
257+
false,
258+
];
259+
} elseif (str($line)->contains([' [200]: GET '])) {
260+
$requestPort = $this->getRequestPortFromLine($line);
261+
262+
$this->requestsPool[$requestPort][1] = trim(explode('[200]: GET', $line)[1]);
263+
} elseif (str($line)->contains(' Closing')) {
264+
$requestPort = $this->getRequestPortFromLine($line);
265+
$request = $this->requestsPool[$requestPort];
266+
267+
[$startDate, $file] = $request;
268+
269+
$formattedStartedAt = $startDate->format('Y-m-d H:i:s');
270+
271+
unset($this->requestsPool[$requestPort]);
272+
273+
[$date, $time] = explode(' ', $formattedStartedAt);
274+
275+
$this->output->write(" <fg=gray>$date</> $time");
276+
277+
$runTime = $this->getDateFromLine($line)->diffInSeconds($startDate);
278+
279+
if ($file) {
280+
$this->output->write($file = " $file");
281+
}
282+
283+
$dots = max(terminal()->width() - mb_strlen($formattedStartedAt) - mb_strlen($file) - mb_strlen($runTime) - 9, 0);
284+
285+
$this->output->write(' '.str_repeat('<fg=gray>.</>', $dots));
286+
$this->output->writeln(" <fg=gray>~ {$runTime}s</>");
287+
} elseif (str($line)->contains(['Closed without sending a request'])) {
288+
// ...
289+
} elseif (! empty($line)) {
290+
$warning = explode('] ', $line);
291+
$this->components->warn(count($warning) > 1 ? $warning[1] : $warning[0]);
292+
}
293+
});
294+
}
295+
296+
/**
297+
* Get the date from the given PHP server output.
298+
*
299+
* @param string $line
300+
* @return \Illuminate\Support\Carbon
301+
*/
302+
protected function getDateFromLine($line)
303+
{
304+
$regex = env('PHP_CLI_SERVER_WORKERS', 1) > 1
305+
? '/^\[\d+]\s\[([a-zA-Z0-9: ]+)\]/'
306+
: '/^\[([^\]]+)\]/';
307+
308+
preg_match($regex, $line, $matches);
309+
310+
return Carbon::createFromFormat('D M d H:i:s Y', $matches[1]);
311+
}
312+
313+
/**
314+
* Get the request port from the given PHP server output.
315+
*
316+
* @param string $line
317+
* @return int
318+
*/
319+
protected function getRequestPortFromLine($line)
320+
{
321+
preg_match('/:(\d+)\s(?:(?:\w+$)|(?:\[.*))/', $line, $matches);
322+
323+
return (int) $matches[1];
324+
}
325+
326+
/**
327+
* Get the console command options.
328+
*
329+
* @return array
330+
*/
331+
protected function getOptions()
332+
{
333+
return [
334+
['host', null, InputOption::VALUE_OPTIONAL, 'The host address to serve the application on', Env::get('SERVER_HOST', '127.0.0.1')],
335+
['port', null, InputOption::VALUE_OPTIONAL, 'The port to serve the application on', Env::get('SERVER_PORT')],
336+
['tries', null, InputOption::VALUE_OPTIONAL, 'The max number of ports to attempt to serve from', 10],
337+
['no-reload', null, InputOption::VALUE_NONE, 'Do not reload the development server on .env file changes'],
338+
];
339+
}
340+
}

0 commit comments

Comments
 (0)