Passed
Pull Request — master (#139)
by Rustam
02:07
created

Serve::execute()   C

Complexity

Conditions 12
Paths 52

Size

Total Lines 64
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12.032

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 12
eloc 32
c 4
b 0
f 0
nc 52
nop 2
dl 0
loc 64
ccs 31
cts 33
cp 0.9394
crap 12.032
rs 6.9666

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\Input\InputArgument;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Console\Style\SymfonyStyle;
13
use Yiisoft\Yii\Console\ExitCode;
14
15
use function explode;
16
use function fclose;
17
use function file_exists;
18
use function fsockopen;
19
use function getcwd;
20
use function is_dir;
21
use function passthru;
22
use function strpos;
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 const DEFAULT_PORT = '8080';
31
    private const DEFAULT_DOCROOT = 'public';
32
    private const DEFAULT_ROUTER = 'public/index.php';
33
34
    protected static $defaultName = 'serve';
35
    protected static $defaultDescription = 'Runs PHP built-in web server';
36
37 45
    public function configure(): void
38
    {
39
        $this
40 45
            ->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.')
41 45
            ->addArgument('address', InputArgument::OPTIONAL, 'Host to serve at', 'localhost')
42 45
            ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port to serve at', self::DEFAULT_PORT)
43 45
            ->addOption('docroot', 't', InputOption::VALUE_OPTIONAL, 'Document root to serve from', self::DEFAULT_DOCROOT)
44 45
            ->addOption('router', 'r', InputOption::VALUE_OPTIONAL, 'Path to router script', self::DEFAULT_ROUTER)
45 45
            ->addOption('env', 'e', InputOption::VALUE_OPTIONAL, 'It is only used for testing.');
46 45
    }
47
48 6
    protected function execute(InputInterface $input, OutputInterface $output): int
49
    {
50 6
        $io = new SymfonyStyle($input, $output);
51
52
        /** @var string $address */
53 6
        $address = $input->getArgument('address');
54
55
        /** @var string $router */
56 6
        $router = $input->getOption('router');
57
58
        /** @var string $port */
59 6
        $port = $input->getOption('port');
60
61
        /** @var string $docroot */
62 6
        $docroot = $input->getOption('docroot');
63
64 6
        if ($router === self::DEFAULT_ROUTER && !file_exists(self::DEFAULT_ROUTER)) {
65 4
            $io->warning('Default router "' . self::DEFAULT_ROUTER . '" does not exist. Serving without router. URLs with dots may fail.');
66 4
            $router = null;
67
        }
68
69
        /** @var string $env */
70 6
        $env = $input->getOption('env');
71
72 6
        $documentRoot = getcwd() . '/' . $docroot; // TODO: can we do it better?
73
74 6
        if (strpos($address, ':') === false) {
75 4
            $address .= ':' . $port;
76
        }
77
78 6
        if (!is_dir($documentRoot)) {
79 1
            $io->error("Document root \"$documentRoot\" does not exist.");
80 1
            return self::EXIT_CODE_NO_DOCUMENT_ROOT;
81
        }
82
83 5
        if ($this->isAddressTaken($address) && $this->isDefaultPort($address)) {
84 1
            $this->findFreeAddress($address);
85
        }
86 5
        if ($this->isAddressTaken($address)) {
87 1
            $io->error("http://$address is taken by another process.");
88 1
            return self::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS;
89
        }
90
91 4
        if ($router !== null && !file_exists($router)) {
92 1
            $io->error("Routing file \"$router\" does not exist.");
93 1
            return self::EXIT_CODE_NO_ROUTING_FILE;
94
        }
95
96 3
        $output->writeLn("Server started on <href=http://$address/>http://$address/</>");
97 3
        $output->writeLn("Document root is \"$documentRoot\"");
98
99 3
        if ($router) {
100 1
            $output->writeLn("Routing file is \"$router\"");
101
        }
102
103 3
        $output->writeLn('Quit the server with CTRL-C or COMMAND-C.');
104
105 3
        if ($env === 'test') {
106 3
            return ExitCode::OK;
107
        }
108
109
        passthru('"' . PHP_BINARY . '"' . " -S $address -t \"$documentRoot\" $router");
110
111
        return ExitCode::OK;
112
    }
113
114
    /**
115
     * @param string $address The server address.
116
     *
117
     * @return bool If address is already in use.
118
     */
119 5
    private function isAddressTaken(string $address): bool
120
    {
121 5
        [$hostname, $port] = explode(':', $address);
122 5
        $fp = @fsockopen($hostname, (int)$port, $errno, $errstr, 3);
123
124 5
        if ($fp === false) {
125 4
            return false;
126
        }
127
128 2
        fclose($fp);
129 2
        return true;
130
    }
131
132 1
    private function findFreeAddress(string &$address): void
133
    {
134 1
        [$hostname, $port] = explode(':', $address);
135 1
        $port = (int)$port;
136 1
        while ($this->isAddressTaken($address)) {
137 1
            $port++;
138 1
            $address = $hostname . ':' . $port;
139
        }
140 1
    }
141
142 2
    private function isDefaultPort(string $address): bool
143
    {
144 2
        [, $port] = explode(':', $address);
145 2
        return $port === self::DEFAULT_PORT;
146
    }
147
}
148