Test Failed
Pull Request — master (#149)
by Rustam
14:44 queued 09:34
created

Serve   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 118
Duplicated Lines 0 %

Test Coverage

Coverage 95.56%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 16
eloc 56
c 4
b 0
f 0
dl 0
loc 118
ccs 43
cts 45
cp 0.9556
rs 10

5 Methods

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