Passed
Pull Request — master (#2062)
by Arnaud
13:15 queued 05:14
created

Serve   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Importance

Changes 6
Bugs 3 Features 0
Metric Value
eloc 169
c 6
b 3
f 0
dl 0
loc 280
rs 9.1199
wmc 41

5 Methods

Rating   Name   Duplication   Size   Complexity  
F execute() 0 172 30
A configure() 0 20 1
A buildSuccess() 0 11 4
A setUpServer() 0 33 4
A tearDownServer() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like Serve often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Serve, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Command;
15
16
use Cecil\Exception\RuntimeException;
17
use Cecil\Util;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputDefinition;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
24
use Symfony\Component\Finder\Finder;
25
use Symfony\Component\Process\Exception\ProcessFailedException;
26
use Symfony\Component\Process\PhpExecutableFinder;
27
use Symfony\Component\Process\Process;
28
use Yosymfony\ResourceWatcher\Crc32ContentHash;
29
use Yosymfony\ResourceWatcher\ResourceCacheMemory;
30
use Yosymfony\ResourceWatcher\ResourceWatcher;
31
32
/**
33
 * Starts the built-in server.
34
 */
35
class Serve extends AbstractCommand
36
{
37
    /**
38
     * {@inheritdoc}
39
     */
40
    protected function configure()
41
    {
42
        $this
43
            ->setName('serve')
44
            ->setDescription('Starts the built-in server')
45
            ->setDefinition(
46
                new InputDefinition([
47
                    new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'),
48
                    new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Set the path to extra config files (comma-separated)'),
49
                    new InputOption('drafts', 'd', InputOption::VALUE_NONE, 'Include drafts'),
50
                    new InputOption('page', 'p', InputOption::VALUE_REQUIRED, 'Build a specific page'),
51
                    new InputOption('open', 'o', InputOption::VALUE_NONE, 'Open web browser automatically'),
52
                    new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Server host'),
53
                    new InputOption('port', null, InputOption::VALUE_REQUIRED, 'Server port'),
54
                    new InputOption('optimize', null, InputOption::VALUE_OPTIONAL, 'Optimize files (disable with "no")', false),
55
                    new InputOption('clear-cache', null, InputOption::VALUE_OPTIONAL, 'Clear cache before build (optional cache key regular expression)', false),
56
                    new InputOption('no-ignore-vcs', null, InputOption::VALUE_NONE, 'Changes watcher must not ignore VCS directories'),
57
                ])
58
            )
59
            ->setHelp('Starts the live-reloading-built-in web server');
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     *
65
     * @throws RuntimeException
66
     */
67
    protected function execute(InputInterface $input, OutputInterface $output)
68
    {
69
        $drafts = $input->getOption('drafts');
70
        $open = $input->getOption('open');
71
        $host = $input->getOption('host') ?? 'localhost';
72
        $port = $input->getOption('port') ?? '8000';
73
        $optimize = $input->getOption('optimize');
74
        $clearcache = $input->getOption('clear-cache');
75
        $verbose = $input->getOption('verbose');
76
        $page = $input->getOption('page');
77
        $noignorevcs = $input->getOption('no-ignore-vcs');
78
79
        $this->setUpServer($host, $port);
80
81
        $phpFinder = new PhpExecutableFinder();
82
        $php = $phpFinder->find();
83
        if ($php === false) {
84
            throw new RuntimeException('Can\'t find a local PHP executable.');
85
        }
86
87
        $command = \sprintf(
88
            '"%s" -S %s:%d -t "%s" "%s"',
89
            $php,
90
            $host,
91
            $port,
92
            Util::joinFile($this->getPath(), (string) $this->getBuilder()->getConfig()->get('output.dir')),
93
            Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php')
94
        );
95
        $process = Process::fromShellCommandline($command);
96
97
        $buildProcessArguments = [
98
            $php,
99
            $_SERVER['argv'][0],
100
        ];
101
        $buildProcessArguments[] = 'build';
102
        $buildProcessArguments[] = $this->getPath();
103
        if (!empty($this->getConfigFiles())) {
104
            $buildProcessArguments[] = '--config';
105
            $buildProcessArguments[] = implode(',', $this->getConfigFiles());
106
        }
107
        if ($drafts) {
108
            $buildProcessArguments[] = '--drafts';
109
        }
110
        if ($optimize === null) {
111
            $buildProcessArguments[] = '--optimize';
112
        }
113
        if (!empty($optimize)) {
114
            $buildProcessArguments[] = '--optimize';
115
            $buildProcessArguments[] = $optimize;
116
        }
117
        if ($clearcache === null) {
118
            $buildProcessArguments[] = '--clear-cache';
119
        }
120
        if (!empty($clearcache)) {
121
            $buildProcessArguments[] = '--clear-cache';
122
            $buildProcessArguments[] = $clearcache;
123
        }
124
        if ($verbose) {
125
            $buildProcessArguments[] = '-' . str_repeat('v', $_SERVER['SHELL_VERBOSITY']);
126
        }
127
        if (!empty($page)) {
128
            $buildProcessArguments[] = '--page';
129
            $buildProcessArguments[] = $page;
130
        }
131
132
        $buildProcess = new Process(
133
            $buildProcessArguments,
134
            null,
135
            ['BOX_REQUIREMENT_CHECKER' => '0'] // prevents double check (build then serve)
136
        );
137
138
        $buildProcess->setTty(Process::isTtySupported());
139
        $buildProcess->setPty(Process::isPtySupported());
140
        $buildProcess->setTimeout(3600 * 2); // timeout = 2 minutes
141
142
        $processOutputCallback = function ($type, $buffer) use ($output) {
143
            $output->write($buffer, false, OutputInterface::OUTPUT_RAW);
144
        };
145
146
        // (re)builds before serve
147
        $output->writeln(\sprintf('<comment>Build process: %s</comment>', implode(' ', $buildProcessArguments)), OutputInterface::VERBOSITY_DEBUG);
148
        $buildProcess->run($processOutputCallback);
149
        if ($buildProcess->isSuccessful()) {
150
            $this->buildSuccess($output);
151
        }
152
        if ($buildProcess->getExitCode() !== 0) {
153
            return 1;
154
        }
155
156
        // handles process
157
        if (!$process->isStarted()) {
158
            // set resource watcher
159
            $finder = new Finder();
160
            $finder->files()
161
                ->in($this->getPath())
162
                ->exclude((string) $this->getBuilder()->getConfig()->get('output.dir'));
163
            if (file_exists(Util::joinFile($this->getPath(), '.gitignore')) && $noignorevcs === false) {
164
                $finder->ignoreVCSIgnored(true);
165
            }
166
            $hashContent = new Crc32ContentHash();
167
            $resourceCache = new ResourceCacheMemory();
168
            $resourceWatcher = new ResourceWatcher($resourceCache, $finder, $hashContent);
169
            $resourceWatcher->initialize();
170
171
            // starts server
172
            try {
173
                if (\function_exists('\pcntl_signal')) {
174
                    pcntl_async_signals(true);
175
                    pcntl_signal(SIGINT, [$this, 'tearDownServer']);
176
                    pcntl_signal(SIGTERM, [$this, 'tearDownServer']);
177
                }
178
                $output->writeln(\sprintf('<comment>Server process: %s</comment>', $command), OutputInterface::VERBOSITY_DEBUG);
179
                $output->writeln(\sprintf('Starting server (<href=http://%s:%d>http://%s:%d</>)...', $host, $port, $host, $port));
180
                $process->start(function ($type, $buffer) {
181
                    if ($type === Process::ERR) {
182
                        error_log($buffer, 3, Util::joinFile($this->getPath(), self::TMP_DIR, 'errors.log'));
183
                    }
184
                });
185
                if ($open) {
186
                    $output->writeln('Opening web browser...');
187
                    Util\Plateform::openBrowser(\sprintf('http://%s:%s', $host, $port));
188
                }
189
                while ($process->isRunning()) {
190
                    sleep(1); // wait for server is ready
191
                    if (!fsockopen($host, (int) $port)) {
192
                        $output->writeln('<info>Server is not ready.</info>');
193
194
                        return 1;
195
                    }
196
                    $watcher = $resourceWatcher->findChanges();
197
                    if ($watcher->hasChanges()) {
198
                        // prints deleted/new/updated files in debug mode
199
                        $output->writeln('<comment>Changes detected.</comment>');
200
                        if (\count($watcher->getDeletedFiles()) > 0) {
201
                            $output->writeln('<comment>Deleted files:</comment>', OutputInterface::VERBOSITY_DEBUG);
202
                            foreach ($watcher->getDeletedFiles() as $file) {
203
                                $output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG);
204
                            }
205
                        }
206
                        if (\count($watcher->getNewFiles()) > 0) {
207
                            $output->writeln('<comment>New files:</comment>', OutputInterface::VERBOSITY_DEBUG);
208
                            foreach ($watcher->getNewFiles() as $file) {
209
                                $output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG);
210
                            }
211
                        }
212
                        if (\count($watcher->getUpdatedFiles()) > 0) {
213
                            $output->writeln('<comment>Updated files:</comment>', OutputInterface::VERBOSITY_DEBUG);
214
                            foreach ($watcher->getUpdatedFiles() as $file) {
215
                                $output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG);
216
                            }
217
                        }
218
                        $output->writeln('');
219
                        // re-builds
220
                        $buildProcess->run($processOutputCallback);
221
                        if ($buildProcess->isSuccessful()) {
222
                            $this->buildSuccess($output);
223
                        }
224
225
                        $output->writeln('<info>Server is runnning...</info>');
226
                    }
227
                }
228
                if ($process->getExitCode() > 0) {
229
                    $output->writeln(\sprintf('<comment>%s</comment>', trim($process->getErrorOutput())));
230
                }
231
            } catch (ProcessFailedException $e) {
232
                $this->tearDownServer();
233
234
                throw new RuntimeException(\sprintf($e->getMessage()));
235
            }
236
        }
237
238
        return 0;
239
    }
240
241
    /**
242
     * Build success.
243
     */
244
    private function buildSuccess(OutputInterface $output): void
245
    {
246
        // writes `changes.flag` file
247
        Util\File::getFS()->dumpFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'changes.flag'), time());
248
        // writes `headers.ini` file
249
        if (null !== $headers = $this->getBuilder()->getConfig()->get('headers')) {
250
            $output->writeln('Writing headers file...');
251
            foreach ($headers as $header) {
252
                Util\File::getFS()->appendToFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'), "[{$header['source']}]\n");
253
                foreach ($header['headers'] as $h) {
254
                    Util\File::getFS()->appendToFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'), "{$h['key']} = \"{$h['value']}\"\n");
255
                }
256
            }
257
        }
258
    }
259
260
    /**
261
     * Prepares server's files.
262
     *
263
     * @throws RuntimeException
264
     */
265
    private function setUpServer(string $host, string $port): void
266
    {
267
        try {
268
            $root = Util::joinFile(__DIR__, '../../');
269
            if (Util\Plateform::isPhar()) {
270
                $root = Util\Plateform::getPharPath() . '/';
271
            }
272
            // copying router
273
            Util\File::getFS()->copy(
274
                $root . '/resources/server/router.php',
275
                Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php'),
276
                true
277
            );
278
            // copying livereload JS
279
            Util\File::getFS()->copy(
280
                $root . '/resources/server/livereload.js',
281
                Util::joinFile($this->getPath(), self::TMP_DIR, 'livereload.js'),
282
                true
283
            );
284
            // copying baseurl text file
285
            Util\File::getFS()->dumpFile(
286
                Util::joinFile($this->getPath(), self::TMP_DIR, 'baseurl'),
287
                \sprintf(
288
                    '%s;%s',
289
                    (string) $this->getBuilder()->getConfig()->get('baseurl'),
290
                    \sprintf('http://%s:%s/', $host, $port)
291
                )
292
            );
293
        } catch (IOExceptionInterface $e) {
294
            throw new RuntimeException(\sprintf('An error occurred while copying server\'s files to "%s".', $e->getPath()));
295
        }
296
        if (!is_file(Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php'))) {
297
            throw new RuntimeException(\sprintf('Router not found: "%s".', Util::joinFile(self::TMP_DIR, 'router.php')));
298
        }
299
    }
300
301
    /**
302
     * Removes temporary directory.
303
     *
304
     * @throws RuntimeException
305
     */
306
    public function tearDownServer(): void
307
    {
308
        $this->output->writeln('');
309
        $this->output->writeln('<info>Server stopped.</info>');
310
311
        try {
312
            Util\File::getFS()->remove(Util::joinFile($this->getPath(), self::TMP_DIR));
313
        } catch (IOExceptionInterface $e) {
314
            throw new RuntimeException($e->getMessage());
315
        }
316
    }
317
}
318