Passed
Pull Request — master (#179)
by Alexander
02:29
created

Serve::execute()   B

Complexity

Conditions 11
Paths 36

Size

Total Lines 67
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 11.3851

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 11
eloc 33
c 5
b 0
f 0
nc 36
nop 2
dl 0
loc 67
ccs 29
cts 34
cp 0.8529
crap 11.3851
rs 7.3166

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('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.')
63 49
            ->addArgument('address', InputArgument::OPTIONAL, 'Host to serve at', $this->defaultAddress)
64 49
            ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port to serve at', $this->defaultPort)
65 49
            ->addOption('docroot', 't', InputOption::VALUE_OPTIONAL, 'Document root to serve from', $this->defaultDocroot)
66 49
            ->addOption('router', 'r', InputOption::VALUE_OPTIONAL, 'Path to router script', $this->defaultRouter)
67 49
            ->addOption('workers', 'w', InputOption::VALUE_OPTIONAL, 'Workers number the server will start with', $this->defaultWorkers)
68 49
            ->addOption('env', 'e', InputOption::VALUE_OPTIONAL, 'It is only used for testing.');
69
    }
70
71 1
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
72
    {
73 1
        if ($input->mustSuggestArgumentValuesFor('address')) {
74 1
            $suggestions->suggestValues(['localhost', '127.0.0.1', '0.0.0.0']);
75 1
            return;
76
        }
77
    }
78
79 6
    protected function execute(InputInterface $input, OutputInterface $output): int
80
    {
81 6
        $io = new SymfonyStyle($input, $output);
82
83
        /** @var string $address */
84 6
        $address = $input->getArgument('address');
85
86
        /** @var string $router */
87 6
        $router = $input->getOption('router');
88 6
        $workers = (int) $input->getOption('workers');
89
90
        /** @var string $port */
91 6
        $port = $input->getOption('port');
92
93
        /** @var string $docroot */
94 6
        $docroot = $input->getOption('docroot');
95
96 6
        if ($router === $this->defaultRouter && !file_exists($this->defaultRouter)) {
97 4
            $io->warning('Default router "' . $this->defaultRouter . '" does not exist. Serving without router. URLs with dots may fail.');
98 4
            $router = null;
99
        }
100
101
        /** @var string $env */
102 6
        $env = $input->getOption('env');
103
104 6
        $documentRoot = $this->getRootPath() . DIRECTORY_SEPARATOR . $docroot;
105
106 6
        if (!str_contains($address, ':')) {
107 5
            $address .= ':' . $port;
108
        }
109
110 6
        if (!is_dir($documentRoot)) {
111 1
            $io->error("Document root \"$documentRoot\" does not exist.");
112 1
            return self::EXIT_CODE_NO_DOCUMENT_ROOT;
113
        }
114
115 5
        if ($this->isAddressTaken($address)) {
116 1
            $io->error("http://$address is taken by another process.");
117 1
            return self::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS;
118
        }
119
120 4
        if ($router !== null && !file_exists($router)) {
121 1
            $io->error("Routing file \"$router\" does not exist.");
122 1
            return self::EXIT_CODE_NO_ROUTING_FILE;
123
        }
124
125 3
        $output->writeLn("Document root is \"$documentRoot\"");
126
127 3
        if ($router) {
128 1
            $output->writeLn("Routing file is \"$router\"");
129
        }
130
131 3
        $output->writeLn('Quit the server with CTRL-C or COMMAND-C.');
132
133 3
        if ($env === 'test') {
134 3
            return ExitCode::OK;
135
        }
136
137
        $command = '"' . PHP_BINARY . '"' . " -S $address -t \"$documentRoot\" $router";
138
139
        if (DIRECTORY_SEPARATOR !== '\\') {
140
            $command = 'PHP_CLI_SERVER_WORKERS=' . $workers . ' ' . $command;
141
        }
142
143
        passthru($command);
144
145
        return ExitCode::OK;
146
    }
147
148
    /**
149
     * @param string $address The server address.
150
     *
151
     * @return bool If address is already in use.
152
     */
153 5
    private function isAddressTaken(string $address): bool
154
    {
155 5
        [$hostname, $port] = explode(':', $address);
156 5
        $fp = @fsockopen($hostname, (int)$port, $errno, $errstr, 3);
157
158 5
        if ($fp === false) {
159 4
            return false;
160
        }
161
162 1
        fclose($fp);
163 1
        return true;
164
    }
165
166 6
    private function getRootPath(): string
167
    {
168 6
        if ($this->appRootPath !== null) {
169
            return rtrim($this->appRootPath, DIRECTORY_SEPARATOR);
170
        }
171
172 6
        return getcwd();
173
    }
174
}
175