Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions formwork/src/Commands/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@

final class ServeCommand implements CommandInterface
{
/**
* @var list<string> List of loopback hosts
*/
private const array LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '::1'];

/**
* @var list<string> List of wildcard hosts
*/
private const array WILDCARD_HOSTS = ['0.0.0.0', '::'];

/**
* Host to bind the server to
*/
private string $host = '127.0.0.1';
private string $host = 'localhost';

/**
* Port to bind the server to
Expand Down Expand Up @@ -66,7 +76,7 @@ public function __invoke(?array $argv = null): never
$this->climate->arguments->add([
'host' => [
'longPrefix' => 'host',
'description' => 'Host to bind the server to',
'description' => 'Host to bind the server to (if the value is omitted, the server will bind to all interfaces)',
'defaultValue' => $this->host,
],
'port' => [
Expand All @@ -89,15 +99,18 @@ public function __invoke(?array $argv = null): never
],
]);

$this->climate->arguments->parse();
// Ignore parsing errors to handle passing `--host` without a value as a flag to bind to all interfaces
@$this->climate->arguments->parse();

if ($this->climate->arguments->get('help')) {
$this->climate->usage($argv);
exit(0);
}

/** @var string */
$host = $this->climate->arguments->get('host');
$host = $this->climate->arguments->defined('host')
? ($this->climate->arguments->get('host') ?: '0.0.0.0') // Bind to all interfaces if `--host` is passed without a value
: $this->host;
Comment on lines +102 to +113
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ error suppression won’t prevent League\CLImate\Arguments::parse() from throwing InvalidArgumentException (as other commands already expect/catch), so --host passed without a value will likely still crash parsing. Also, parse() is called without $argv, meaning __invoke($argv) won’t be honored consistently. Consider passing $argv to parse() and handling the specific “missing value for --host” case via try/catch (or by pre-processing $argv to rewrite bare --host into --host=0.0.0.0), while still surfacing other parse errors to the user.

Copilot uses AI. Check for mistakes.

/** @var int */
$port = $this->climate->arguments->get('port');
Expand Down Expand Up @@ -164,6 +177,14 @@ private function handleOutput(string $type, array $lines): void
$this->climate->br();
$this->climate->out(sprintf('➜ Listening on <cyan>http://%s:<bold>%s</bold>/</cyan>', $this->formatHost($this->host), $this->port));
$this->climate->br();

if (in_array($this->host, self::WILDCARD_HOSTS, true)) {
foreach ($this->getLocalNetworkIps() as $localNetworkIp) {
$this->climate->out(sprintf('➜ Remote address: <cyan>http://%s:<bold>%s</bold>/</cyan>', $this->formatHost($localNetworkIp), $this->port));
$this->climate->br();
}
}

$this->climate->out('<dark_gray>Press <bold>CTRL+C</bold> to stop</dark_gray>');
$this->climate->br();
break;
Expand Down Expand Up @@ -336,4 +357,34 @@ private function outputRawLine(string $type, string $line): void
throw new UnexpectedValueException(sprintf('Unexpected output type "%s"', $type));
}
}

/**
* Get local network IP addresses, excluding loopback interfaces
*
* @return list<string>
*/
private function getLocalNetworkIps(): array
{
if (($interfaces = net_get_interfaces()) === false) {
return [];
}

$localNetworkIps = [];

foreach ($interfaces as $interface) {
if (!isset($interface['unicast'])) {
continue;
}
foreach ($interface['unicast'] as $data) {
if (
$data['family'] === AF_INET // IPv4 addresses only
&& !in_array($data['address'], self::LOOPBACK_HOSTS, true) // Exclude loopback address
) {
Comment on lines +368 to +382
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

net_get_interfaces() (and the AF_INET constant) require the sockets extension; the project doesn’t declare ext-sockets in composer requirements, so this can fatally error on systems without it (especially when binding to a wildcard host, which is now a supported path). Please guard with function_exists('net_get_interfaces') / defined('AF_INET') (or extension_loaded('sockets')) and return an empty list (or a helpful message) when unavailable.

Copilot uses AI. Check for mistakes.
$localNetworkIps[] = $data['address'];
}
}
}

return $localNetworkIps;
}
}