Passed
Push — master ( c2b69d...ced3fe )
by Caen
03:36
created

ServeCommand   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 217
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 85
dl 0
loc 217
rs 9.0399
c 0
b 0
f 0
wmc 42

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getOpenCommand() 0 7 1
A configureOutput() 0 4 2
A useBasicOutput() 0 3 2
A getPortSelection() 0 3 2
A getHostSelection() 0 3 2
A parseEnvironmentOption() 0 13 2
A isPortAvailable() 0 14 3
A checkArgvForOption() 0 9 3
A getOutputHandler() 0 5 2
A getExecutablePath() 0 3 1
A runViteProcess() 0 12 2
A printStartMessage() 0 5 2
A openInBrowser() 0 13 5
A cleanupViteHotFile() 0 6 2
A safeHandle() 0 26 4
A handleRunningProcesses() 0 6 2
A getEnvironmentVariables() 0 8 1
A runServerProcess() 0 3 1
A handleViteOutput() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like ServeCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ServeCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\RealtimeCompiler\Console\Commands;
6
7
use Closure;
8
use Hyde\Facades\Filesystem;
9
use Hyde\Hyde;
10
use Hyde\Facades\Config;
11
use Illuminate\Contracts\Process\InvokedProcess;
12
use Illuminate\Support\Arr;
13
use Illuminate\Support\Sleep;
14
use InvalidArgumentException;
15
use Hyde\Console\Concerns\Command;
16
use Hyde\RealtimeCompiler\ConsoleOutput;
17
use Illuminate\Support\Facades\Process;
18
19
use function rtrim;
20
use function sprintf;
21
use function in_array;
22
use function str_replace;
23
use function class_exists;
24
25
/**
26
 * Start the realtime compiler server.
27
 *
28
 * @see https://github.com/hydephp/realtime-compiler
29
 */
30
class ServeCommand extends Command
31
{
32
    /** @var string */
33
    protected $signature = 'serve
34
        {--host= : <comment>[default: "localhost"]</comment>}}
35
        {--port= : <comment>[default: 8080]</comment>}
36
        {--save-preview= : Should the served page be saved to disk? (Overrides config setting)}
37
        {--dashboard= : Enable the realtime compiler dashboard. (Overrides config setting)}
38
        {--pretty-urls= : Enable pretty URLs. (Overrides config setting)}
39
        {--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)}
40
        {--open=false : Open the site preview in the browser.}
41
        {--vite : Enable Vite for Hot Module Replacement (HMR)}
42
    ';
43
44
    /** @var string */
45
    protected $description = 'Start the realtime compiler server';
46
47
    protected ConsoleOutput $console;
48
49
    protected InvokedProcess $server;
50
    protected ?InvokedProcess $vite = null;
51
52
    public function safeHandle(): int
53
    {
54
        $this->configureOutput();
55
        $this->printStartMessage();
56
57
        if ($this->option('open') !== 'false') {
58
            $this->openInBrowser((string) $this->option('open'));
59
        }
60
61
        if ($this->option('vite')) {
62
            $this->runViteProcess();
63
        }
64
65
        $this->runServerProcess(sprintf('php -S %s:%d %s',
66
            $this->getHostSelection(),
67
            $this->getPortSelection(),
68
            escapeshellarg($this->getExecutablePath()),
69
        ));
70
71
        $this->handleRunningProcesses();
72
73
        if ($this->option('vite')) {
74
            $this->cleanupViteHotFile();
75
        }
76
77
        return Command::SUCCESS;
78
    }
79
80
    protected function getHostSelection(): string
81
    {
82
        return (string) $this->option('host') ?: Config::getString('hyde.server.host', 'localhost');
83
    }
84
85
    protected function getPortSelection(): int
86
    {
87
        return (int) ($this->option('port') ?: Config::getInt('hyde.server.port', 8080));
88
    }
89
90
    protected function getExecutablePath(): string
91
    {
92
        return Hyde::path('vendor/hyde/realtime-compiler/bin/server.php');
0 ignored issues
show
Bug introduced by
The method path() does not exist on Hyde\Hyde. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
        return Hyde::/** @scrutinizer ignore-call */ path('vendor/hyde/realtime-compiler/bin/server.php');
Loading history...
93
    }
94
95
    protected function runServerProcess(string $command): void
96
    {
97
        $this->server = Process::forever()->env($this->getEnvironmentVariables())->start($command, $this->getOutputHandler());
98
    }
99
100
    protected function getEnvironmentVariables(): array
101
    {
102
        return Arr::whereNotNull([
103
            'HYDE_SERVER_REQUEST_OUTPUT' => ! $this->option('no-ansi'),
104
            'HYDE_SERVER_SAVE_PREVIEW' => $this->parseEnvironmentOption('save-preview'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('save-preview') targeting Hyde\RealtimeCompiler\Co...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
105
            'HYDE_SERVER_DASHBOARD' => $this->parseEnvironmentOption('dashboard'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('dashboard') targeting Hyde\RealtimeCompiler\Co...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
106
            'HYDE_PRETTY_URLS' => $this->parseEnvironmentOption('pretty-urls'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('pretty-urls') targeting Hyde\RealtimeCompiler\Co...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
107
            'HYDE_PLAY_CDN' => $this->parseEnvironmentOption('play-cdn'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('play-cdn') targeting Hyde\RealtimeCompiler\Co...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
108
        ]);
109
    }
110
111
    protected function configureOutput(): void
112
    {
113
        if (! $this->useBasicOutput()) {
114
            $this->console = new ConsoleOutput($this->output->isVerbose());
115
        }
116
    }
117
118
    protected function printStartMessage(): void
119
    {
120
        $this->useBasicOutput()
121
            ? $this->output->writeln('<info>Starting the HydeRC server...</info> Use Ctrl+C to stop')
122
            : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables(), $this->option('vite'));
0 ignored issues
show
Bug introduced by
$this->option('vite') of type string is incompatible with the type boolean|null expected by parameter $willUseVite of Hyde\RealtimeCompiler\Co...ut::printStartMessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
            : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables(), /** @scrutinizer ignore-type */ $this->option('vite'));
Loading history...
123
    }
124
125
    protected function getOutputHandler(): Closure
126
    {
127
        return $this->useBasicOutput() ? function (string $type, string $line): void {
128
            $this->output->write($line);
129
        } : $this->console->getFormatter();
130
    }
131
132
    protected function useBasicOutput(): bool
133
    {
134
        return $this->option('no-ansi') || ! class_exists(ConsoleOutput::class);
135
    }
136
137
    protected function parseEnvironmentOption(string $name): ?string
138
    {
139
        $value = $this->option($name) ?? $this->checkArgvForOption($name);
140
141
        if ($value !== null) {
142
            return match ($value) {
143
                'true', '' => 'enabled',
144
                'false' => 'disabled',
145
                default => throw new InvalidArgumentException(sprintf('Invalid boolean value for --%s option.', $name))
146
            };
147
        }
148
149
        return null;
150
    }
151
152
    /** Fallback check so that an environment option without a value is acknowledged as true. */
153
    protected function checkArgvForOption(string $name): ?string
154
    {
155
        if (isset($_SERVER['argv'])) {
156
            if (in_array("--$name", $_SERVER['argv'], true)) {
157
                return 'true';
158
            }
159
        }
160
161
        return null;
162
    }
163
164
    protected function openInBrowser(string $path = '/'): void
165
    {
166
        $binary = $this->getOpenCommand(PHP_OS_FAMILY);
167
168
        $command = sprintf('%s http://%s:%d', $binary, $this->getHostSelection(), $this->getPortSelection());
169
        $command = rtrim("$command/$path", '/');
170
171
        $process = $binary ? Process::command($command)->run() : null;
172
173
        if (! $process || $process->failed()) {
174
            $this->warn('Unable to open the site preview in the browser on your system:');
175
            $this->line(sprintf('  %s', str_replace("\n", "\n  ", $process ? $process->errorOutput() : "Missing suitable 'open' binary.")));
176
            $this->newLine();
177
        }
178
    }
179
180
    protected function getOpenCommand(string $osFamily): ?string
181
    {
182
        return match ($osFamily) {
183
            'Windows' => 'start',
184
            'Darwin' => 'open',
185
            'Linux' => 'xdg-open',
186
            default => null
187
        };
188
    }
189
190
    protected function runViteProcess(): void
191
    {
192
        if (! $this->isPortAvailable(5173)) {
193
            throw new InvalidArgumentException(
194
                'Unable to start Vite server: Port 5173 is already in use. '.
195
                'Please stop any other Vite processes and try again.'
196
            );
197
        }
198
199
        Filesystem::touch('app/storage/framework/runtime/vite.hot');
200
201
        $this->vite = Process::forever()->start('npm run dev');
202
    }
203
204
    protected function handleRunningProcesses(): void
205
    {
206
        while ($this->server->running()) {
207
            $this->handleViteOutput();
208
209
            Sleep::for(100)->milliseconds();
210
        }
211
    }
212
213
    protected function handleViteOutput(): void
214
    {
215
        if ($this->vite?->running()) {
216
            $output = $this->vite->latestOutput();
0 ignored issues
show
Bug introduced by
The method latestOutput() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

216
            /** @scrutinizer ignore-call */ 
217
            $output = $this->vite->latestOutput();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
217
218
            if ($output) {
219
                $this->output->write($output);
220
            }
221
        }
222
    }
223
224
    /** @experimental This feature may be removed before the final release. */
225
    protected function isPortAvailable(int $port): bool
226
    {
227
        $addresses = ['localhost', '127.0.0.1'];
228
229
        foreach ($addresses as $address) {
230
            $socket = @fsockopen($address, $port, $errno, $errstr, 1);
231
            if ($socket !== false) {
232
                fclose($socket);
233
234
                return false;
235
            }
236
        }
237
238
        return true;
239
    }
240
241
    protected function cleanupViteHotFile(): void
242
    {
243
        $hotFile = 'app/storage/framework/runtime/vite.hot';
244
245
        if (Filesystem::exists($hotFile)) {
246
            Filesystem::unlinkIfExists($hotFile);
247
        }
248
    }
249
}
250