Test Failed
Pull Request — master (#207)
by Dmitriy
03:29 queued 01:06
created

Serve   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 193
Duplicated Lines 0 %

Test Coverage

Coverage 96.26%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 26
eloc 104
c 4
b 0
f 0
dl 0
loc 193
rs 10
ccs 103
cts 107
cp 0.9626
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Console\Command;
6
7
use Symfony\Component\Console\Attribute\AsCommand;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Completion\CompletionInput;
10
use Symfony\Component\Console\Completion\CompletionSuggestions;
11
use Symfony\Component\Console\Input\InputArgument;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use Symfony\Component\Console\Style\SymfonyStyle;
16
use Yiisoft\Yii\Console\ExitCode;
17
18
use function explode;
19
use function fclose;
20
use function file_exists;
21
use function fsockopen;
22
use function is_dir;
23
use function passthru;
24
25
use function explode;
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use explode as explode because the name is already in use
Loading history...
26
use function fclose;
27
use function file_exists;
28
use function fsockopen;
29
use function is_dir;
30
use function passthru;
31
32
#[AsCommand('serve', 'Runs PHP built-in web server')]
33
final class Serve extends Command
34
{
35
    public const EXIT_CODE_NO_DOCUMENT_ROOT = 2;
36
    public const EXIT_CODE_NO_ROUTING_FILE = 3;
37
    public const EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS = 5;
38
39
    private string $defaultAddress;
40
    private string $defaultPort;
41
    private string $defaultDocroot;
42
    private string $defaultRouter;
43
    private int $defaultWorkers;
44
45
    /**
46
     * @psalm-param array{
47 49
     *     address?:non-empty-string,
48
     *     port?:non-empty-string,
49 49
     *     docroot?:string,
50 49
     *     router?:string,
51 49
     *     workers?:int|string
52 49
     * } $options
53 49
     */
54
    public function __construct(private ?string $appRootPath = null, ?array $options = [])
55 49
    {
56
        $this->defaultAddress = $options['address'] ?? '127.0.0.1';
57
        $this->defaultPort = $options['port'] ?? '8080';
58 49
        $this->defaultDocroot = $options['docroot'] ?? 'public';
59
        $this->defaultRouter = $options['router'] ?? 'public/index.php';
60 49
        $this->defaultWorkers = (int) ($options['workers'] ?? 2);
61 49
62 49
        parent::__construct();
63 49
    }
64 49
65 49
    public function configure(): void
66 49
    {
67 49
        $this
68 49
            ->setHelp(
69 49
                'In order to access server from remote machines use 0.0.0.0:8000. That is especially useful when running server in a virtual machine.'
70 49
            )
71 49
            ->addArgument('address', InputArgument::OPTIONAL, 'Host to serve at', $this->defaultAddress)
72 49
            ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port to serve at', $this->defaultPort)
73 49
            ->addOption(
74 49
                'docroot',
75 49
                't',
76 49
                InputOption::VALUE_OPTIONAL,
77 49
                'Document root to serve from',
78 49
                $this->defaultDocroot
79 49
            )
80 49
            ->addOption('router', 'r', InputOption::VALUE_OPTIONAL, 'Path to router script', $this->defaultRouter)
81 49
            ->addOption(
82 49
                'workers',
83
                'w',
84
                InputOption::VALUE_OPTIONAL,
85 1
                'Workers number the server will start with',
86
                $this->defaultWorkers
87 1
            )
88 1
            ->addOption('env', 'e', InputOption::VALUE_OPTIONAL, 'It is only used for testing.')
89 1
            ->addOption('open', 'o', InputOption::VALUE_OPTIONAL, 'Opens the serving server in the default browser.')
90
            ->addOption('xdebug', 'x', InputOption::VALUE_OPTIONAL, 'Enables XDEBUG session.', false);
91
    }
92
93 6
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
94
    {
95 6
        if ($input->mustSuggestArgumentValuesFor('address')) {
96 6
            $suggestions->suggestValues(['localhost', '127.0.0.1', '0.0.0.0']);
97 6
            return;
98
        }
99
    }
100 6
101
    protected function execute(InputInterface $input, OutputInterface $output): int
102
    {
103 6
        $io = new SymfonyStyle($input, $output);
104 6
        $io->title('Yii3 Development Server');
105
        $io->writeln('https://yiiframework.com' . "\n");
106
107 6
        /** @var string $address */
108
        $address = $input->getArgument('address');
109
110 6
        /** @var string $router */
111
        $router = $input->getOption('router');
112 6
        $workers = (int) $input->getOption('workers');
113 4
114 4
        /** @var string $port */
115 4
        $port = $input->getOption('port');
116 4
117
        /** @var string $docroot */
118
        $docroot = $input->getOption('docroot');
119
120 6
        if ($router === $this->defaultRouter && !file_exists($this->defaultRouter)) {
121
            $io->warning(
122 6
                'Default router "' . $this->defaultRouter . '" does not exist. Serving without router. URLs with dots may fail.'
123
            );
124 6
            $router = null;
125 5
        }
126
127
        /** @var string $env */
128 6
        $env = $input->getOption('env');
129 1
130 1
        $documentRoot = $this->getRootPath() . DIRECTORY_SEPARATOR . $docroot;
131
132
        if (!str_contains($address, ':')) {
133 5
            $address .= ':' . $port;
134 1
        }
135 1
136
        if (!is_dir($documentRoot)) {
137
            $io->error("Document root \"$documentRoot\" does not exist.");
138 4
            return self::EXIT_CODE_NO_DOCUMENT_ROOT;
139 1
        }
140 1
141
        if ($this->isAddressTaken($address)) {
142
            $io->error("http://$address is taken by another process.");
143 3
            return self::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS;
144
        }
145 3
146
        if ($router !== null && !file_exists($router)) {
147 3
            $io->error("Routing file \"$router\" does not exist.");
148 3
            return self::EXIT_CODE_NO_ROUTING_FILE;
149
        }
150
151 3
        $command = [];
152 3
153
        $isLinux = DIRECTORY_SEPARATOR !== '\\';
154 3
155
        if ($isLinux) {
156
            $command[] = 'PHP_CLI_SERVER_WORKERS=' . $workers;
157 3
        }
158 3
159 3
        $xDebugInstalled = extension_loaded('xdebug');
160 3
        $xDebugEnabled = $isLinux && $xDebugInstalled && $input->hasOption('xdebug') && $input->getOption('xdebug') === null;
161 3
162 3
        if ($xDebugEnabled) {
163 3
            $command[] = 'XDEBUG_MODE=debug XDEBUG_TRIGGER=yes';
164 3
        }
165 3
        $outputTable = [];
166 3
        $outputTable[] = ['PHP', PHP_VERSION];
167 3
        $outputTable[] = [
168 3
            'xDebug',
169 3
            $xDebugInstalled ? sprintf(
170 3
                '%s, %s',
171
                phpversion('xdebug'),
172 3
                $xDebugEnabled ? '<info> Enabled </>' : '<error> Disabled </>',
173
            ) : '<error>Not installed</>',
174 3
            '--xdebug',
175 3
        ];
176
        $outputTable[] = ['Workers', $isLinux ? $workers : 'Not supported', '--workers, -w'];
177 3
        $outputTable[] = ['Address', $address];
178 3
        $outputTable[] = ['Document root', $documentRoot, '--docroot, -t'];
179 3
        $outputTable[] = ($router ? ['Routing file', $router, '--router, -r'] : []);
180 3
181
        $io->table(['Configuration', null, 'Options'], $outputTable);
182 3
183
        $command[] = '"' . PHP_BINARY . '"' . " -S $address -t \"$documentRoot\" $router";
184 3
        $command = implode(' ', $command);
185 3
186
        $output->writeln([
187
            'Executing: ',
188
            sprintf('<info>%s</>', $command),
189
        ], OutputInterface::VERBOSITY_VERBOSE);
190
191
        $io->success('Quit the server with CTRL-C or COMMAND-C.');
192
193
        if ($env === 'test') {
194
            return ExitCode::OK;
195
        }
196
197
        $openInBrowser = $input->hasOption('open') && $input->getOption('open') === null;
198 5
199
        if ($openInBrowser) {
200 5
            passthru('open http://' . $address);
201 5
        }
202
        passthru($command, $result);
203 5
204 4
        return $result;
205
    }
206
207 1
    /**
208 1
     * @param string $address The server address.
209
     *
210
     * @return bool If address is already in use.
211 6
     */
212
    private function isAddressTaken(string $address): bool
213 6
    {
214
        [$hostname, $port] = explode(':', $address);
215
        $fp = @fsockopen($hostname, (int) $port, $errno, $errstr, 3);
216
217 6
        if ($fp === false) {
218
            return false;
219
        }
220
221
        fclose($fp);
222
        return true;
223
    }
224
225
    private function getRootPath(): string
226
    {
227
        if ($this->appRootPath !== null) {
228
            return rtrim($this->appRootPath, DIRECTORY_SEPARATOR);
229
        }
230
231
        return getcwd();
232
    }
233
}
234