Passed
Push — master ( 9638a7...1ba969 )
by Alexander
02:32
created

Serve::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 10
ccs 8
cts 8
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Yiisoft\Yii\Console\Command;
4
5
use Symfony\Component\Console\Command\Command;
6
use Symfony\Component\Console\Input\InputInterface;
7
use Symfony\Component\Console\Input\InputOption;
8
use Symfony\Component\Console\Input\InputArgument;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Style\SymfonyStyle;
11
use Yiisoft\Yii\Console\ExitCode;
12
13
class Serve extends Command
14
{
15
    public const EXIT_CODE_NO_DOCUMENT_ROOT = 2;
16
    public const EXIT_CODE_NO_ROUTING_FILE = 3;
17
    public const EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS = 5;
18
19
    private const DEFAULT_PORT = 8080;
20
    private const DEFAULT_DOCROOT = 'public';
21
22
    protected static $defaultName = 'serve';
23
24 2
    public function configure(): void
25
    {
26
        $this
27 2
            ->setDescription('Runs PHP built-in web server')
28 2
            ->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.')
29 2
            ->addArgument('address', InputArgument::OPTIONAL, 'Host to serve at', 'localhost')
30 2
            ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port to serve at', self::DEFAULT_PORT)
31 2
            ->addOption('docroot', 't', InputOption::VALUE_OPTIONAL, 'Document root to serve from', self::DEFAULT_DOCROOT)
32 2
            ->addOption('router', 'r', InputOption::VALUE_OPTIONAL, 'Path to router script')
33 2
            ->addOption('env', 'e', InputOption::VALUE_OPTIONAL, 'It is only used for testing.');
34 2
    }
35
36 2
    protected function execute(InputInterface $input, OutputInterface $output)
37
    {
38 2
        $io = new SymfonyStyle($input, $output);
39 2
        $address = $input->getArgument('address');
40
41 2
        $port = $input->getOption('port');
42 2
        $docroot = $input->getOption('docroot');
43 2
        $router = $input->getOption('router');
44 2
        $env = $input->getOption('env');
45
46 2
        $documentRoot = getcwd() . '/' . $docroot; // TODO: can we do it better?
47
48 2
        if (strpos($address, ':') === false) {
0 ignored issues
show
Bug introduced by
It seems like $address can also be of type string[]; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

48
        if (strpos(/** @scrutinizer ignore-type */ $address, ':') === false) {
Loading history...
49 2
            $address .= ':' . $port;
50
        }
51
52 2
        if (!is_dir($documentRoot)) {
53 1
            $io->error("Document root \"$documentRoot\" does not exist.");
54 1
            return self::EXIT_CODE_NO_DOCUMENT_ROOT;
55
        }
56
57 1
        if ($this->isAddressTaken($address)) {
0 ignored issues
show
Bug introduced by
It seems like $address can also be of type null and string[]; however, parameter $address of Yiisoft\Yii\Console\Comm...Serve::isAddressTaken() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

57
        if ($this->isAddressTaken(/** @scrutinizer ignore-type */ $address)) {
Loading history...
58
            $io->error("http://$address is taken by another process.");
59
            return self::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS;
60
        }
61
62 1
        if ($router !== null && !file_exists($router)) {
0 ignored issues
show
Bug introduced by
It seems like $router can also be of type string[]; however, parameter $filename of file_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

62
        if ($router !== null && !file_exists(/** @scrutinizer ignore-type */ $router)) {
Loading history...
63
            $io->error("Routing file \"$router\" does not exist.");
64
            return self::EXIT_CODE_NO_ROUTING_FILE;
65
        }
66
67 1
        $output->writeLn("Server started on http://{$address}/");
68 1
        $output->writeLn("Document root is \"{$documentRoot}\"");
69 1
        if ($router) {
70
            $output->writeLn("Routing file is \"$router\"");
71
        }
72 1
        $output->writeLn('Quit the server with CTRL-C or COMMAND-C.');
73
74 1
        if ($env === 'test') {
75 1
            return ExitCode::OK;
76
        }
77
78
        passthru('"' . PHP_BINARY . '"' . " -S {$address} -t \"{$documentRoot}\" $router");
79
    }
80
81
    /**
82
     * @param string $address server address
83
     * @return bool if address is already in use
84
     */
85 1
    private function isAddressTaken(string $address): bool
86
    {
87 1
        [$hostname, $port] = explode(':', $address);
88 1
        $fp = @fsockopen($hostname, $port, $errno, $errstr, 3);
0 ignored issues
show
Bug introduced by
$port of type string is incompatible with the type integer expected by parameter $port of fsockopen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        $fp = @fsockopen($hostname, /** @scrutinizer ignore-type */ $port, $errno, $errstr, 3);
Loading history...
89 1
        if ($fp === false) {
90 1
            return false;
91
        }
92
        fclose($fp);
93
        return true;
94
    }
95
}
96