Test Failed
Pull Request — master (#180)
by Dmitriy
12:16
created

Serve::execute()   F

Complexity

Conditions 17
Paths 400

Size

Total Lines 99
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 18.0513

Importance

Changes 5
Bugs 0 Features 1
Metric Value
eloc 58
c 5
b 0
f 1
dl 0
loc 99
ccs 33
cts 39
cp 0.8462
rs 1.8833
cc 17
nc 400
nop 2
crap 18.0513

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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