Passed
Pull Request — master (#2226)
by Arnaud
09:02 queued 03:18
created

AbstractCommand::run()   B

Complexity

Conditions 11
Paths 40

Size

Total Lines 42
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 24
nc 40
nop 2
dl 0
loc 42
rs 7.3166
c 0
b 0
f 0

How to fix   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
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cecil\Command;
15
16
use Cecil\Builder;
17
use Cecil\Config;
18
use Cecil\Exception\RuntimeException;
19
use Cecil\Logger\ConsoleLogger;
20
use Cecil\Util;
21
use Joli\JoliNotif\Notification;
22
use Symfony\Component\Console\Command\Command;
23
use Symfony\Component\Console\Input\InputInterface;
24
use Symfony\Component\Console\Output\OutputInterface;
25
use Symfony\Component\Console\Style\SymfonyStyle;
26
use Symfony\Component\Filesystem\Path;
27
use Symfony\Component\Process\Process;
28
use Symfony\Component\Validator\Constraints\Url;
29
use Symfony\Component\Validator\Validation;
30
31
/**
32
 * Abstract command class.
33
 *
34
 * This class provides common functionality for all commands, such as configuration loading, path handling, and error management.
35
 */
36
class AbstractCommand extends Command
37
{
38
    public const CONFIG_FILE = ['cecil.yml', 'config.yml'];
39
    public const TMP_DIR = '.cecil';
40
    public const EXCLUDED_CMD = ['about', 'new:site', 'self-update'];
41
    public const SERVE_OUTPUT = '.cecil/preview';
42
43
    /** @var Notification */
44
    public $notification;
45
46
    /** @var InputInterface */
47
    protected $input;
48
49
    /** @var OutputInterface */
50
    protected $output;
51
52
    /** @var SymfonyStyle */
53
    protected $io;
54
55
    /** @var string */
56
    protected $rootPath;
57
58
    /** @var null|string */
59
    private $path = null;
60
61
    /** @var array */
62
    private $configFiles = [];
63
64
    /** @var Config */
65
    private $config;
66
67
    /** @var Builder */
68
    private $builder;
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    protected function initialize(InputInterface $input, OutputInterface $output)
74
    {
75
        $this->input = $input;
76
        $this->output = $output;
77
        $this->io = new SymfonyStyle($input, $output);
78
        $this->rootPath = Util\Platform::isPhar() ? Util\Platform::getPharPath() . '/' : realpath(Util::joinFile(__DIR__, '/../../'));
79
80
        // prepare configuration files list
81
        if (!\in_array($this->getName(), self::EXCLUDED_CMD)) {
82
            // site config file
83
            $this->configFiles[$this->locateConfigFile($this->getPath())['name']] = $this->locateConfigFile($this->getPath())['path'];
84
            // additional config file(s) from --config=<file>
85
            if ($input->hasOption('config') && $input->getOption('config') !== null) {
86
                $this->configFiles += $this->locateAdditionalConfigFiles($this->getPath(), (string) $input->getOption('config'));
87
            }
88
            // checks file(s)
89
            $this->configFiles = array_unique($this->configFiles);
90
            foreach ($this->configFiles as $fileName => $filePath) {
91
                if ($filePath === false) {
92
                    unset($this->configFiles[$fileName]);
93
                    $this->io->warning(\sprintf('Could not find configuration file "%s".', $fileName));
94
                }
95
            }
96
        }
97
        // prepare notification
98
        $this->notification = (new Notification())
99
            ->setTitle('Cecil')
100
            ->setIcon('./resources/icon.png')
101
            ->setBody('Notification from Cecil static site generator.')
102
        ;
103
104
        parent::initialize($input, $output);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    public function run(InputInterface $input, OutputInterface $output): int
111
    {
112
        // disable debug mode if a verbosity level is specified
113
        if ($output->getVerbosity() != OutputInterface::VERBOSITY_NORMAL) {
114
            putenv('CECIL_DEBUG=false');
115
        }
116
        // force verbosity level to "debug" in debug mode
117
        if (getenv('CECIL_DEBUG') == 'true') {
118
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
119
        }
120
        if ($output->isDebug()) {
121
            // set env. variable in debug mode
122
            putenv('CECIL_DEBUG=true');
123
124
            return parent::run($input, $output);
125
        }
126
        // run with human error message
127
        try {
128
            return parent::run($input, $output);
129
        } catch (\Exception $e) {
130
            if ($this->io === null) {
131
                $this->io = new SymfonyStyle($input, $output);
132
            }
133
            $message = '';
134
            $i = 0;
135
            do {
136
                //if ($e instanceof \Twig\Error\RuntimeError) {
137
                //    continue;
138
                //}
139
140
                if ($i > 0) {
141
                    $message .= '└ ';
142
                }
143
                $message .= "{$e->getMessage()}\n";
144
                if ($e->getFile() && $e instanceof RuntimeException) {
145
                    $message .= \sprintf("→ %s%s\n", $e->getFile(), $e->getLine() ? ":{$e->getLine()}" : '');
146
                }
147
                $i++;
148
            } while ($e = $e->getPrevious());
149
            $this->io->error($message);
150
151
            exit(1);
152
        }
153
    }
154
155
    /**
156
     * Returns the working path.
157
     */
158
    protected function getPath(bool $exist = true): ?string
159
    {
160
        if ($this->path === null) {
161
            try {
162
                // get working directory by default
163
                if (false === $this->path = getcwd()) {
164
                    throw new \Exception('Unable to get current working directory.');
165
                }
166
                // ... or path
167
                if ($this->input->getArgument('path') !== null) {
168
                    $this->path = Path::canonicalize($this->input->getArgument('path'));
169
                }
170
                // try to get canonicalized absolute path
171
                if ($exist) {
172
                    if (realpath($this->path) === false) {
173
                        throw new \Exception(\sprintf('The given path "%s" is not valid.', $this->path));
174
                    }
175
                    $this->path = realpath($this->path);
176
                }
177
            } catch (\Exception $e) {
178
                throw new \Exception($e->getMessage());
179
            }
180
        }
181
182
        return $this->path;
183
    }
184
185
    /**
186
     * Returns config file(s) path.
187
     */
188
    protected function getConfigFiles(): array
189
    {
190
        return $this->configFiles ?? [];
191
    }
192
193
    /**
194
     * Creates or returns a Builder instance.
195
     *
196
     * @throws RuntimeException
197
     */
198
    protected function getBuilder(array $config = []): Builder
199
    {
200
        try {
201
            // loads configuration files if not already done
202
            if ($this->config === null) {
203
                $this->config = new Config();
204
                // loads and merges configuration files
205
                foreach ($this->getConfigFiles() as $filePath) {
206
                    $this->config->import($this->config::loadFile($filePath), Config::IMPORT_MERGE);
207
                }
208
                // merges configuration from $config parameter
209
                $this->config->import($config, Config::IMPORT_MERGE);
210
            }
211
            // creates builder instance if not already done
212
            if ($this->builder === null) {
213
                $this->builder = (new Builder($this->config, new ConsoleLogger($this->output)))
214
                    ->setSourceDir($this->getPath())
215
                    ->setDestinationDir($this->getPath());
216
            }
217
        } catch (\Exception $e) {
218
            throw new RuntimeException($e->getMessage());
219
        }
220
221
        return $this->builder;
222
    }
223
224
    /**
225
     * Locates the configuration in the given path, as an array of the file name and path, if file exists, otherwise default name and false.
226
     */
227
    protected function locateConfigFile(string $path): array
228
    {
229
        $config = [
230
            'name' => self::CONFIG_FILE[0],
231
            'path' => false,
232
        ];
233
        foreach (self::CONFIG_FILE as $configFileName) {
234
            if (($configFilePath = realpath(Util::joinFile($path, $configFileName))) !== false) {
235
                $config = [
236
                    'name' => $configFileName,
237
                    'path' => $configFilePath,
238
                ];
239
            }
240
        }
241
242
        return $config;
243
    }
244
245
    /**
246
     * Locates additional configuration file(s) from the given list of files, relative to the given path or absolute.
247
     */
248
    protected function locateAdditionalConfigFiles(string $path, string $configFilesList): array
249
    {
250
        $config = [];
251
        foreach (explode(',', $configFilesList) as $filename) {
252
            // absolute path
253
            $config[$filename] = realpath($filename);
254
            // relative path
255
            if (!Util\File::getFS()->isAbsolutePath($filename)) {
256
                $config[$filename] = realpath(Util::joinFile($path, $filename));
257
            }
258
        }
259
260
        return $config;
261
    }
262
263
    /**
264
     * Opens path with editor.
265
     *
266
     * @throws RuntimeException
267
     */
268
    protected function openEditor(string $path, string $editor): void
269
    {
270
        $command = \sprintf('%s "%s"', $editor, $path);
271
        switch (Util\Platform::getOS()) {
272
            case Util\Platform::OS_WIN:
273
                $command = \sprintf('start /B "" %s "%s"', $editor, $path);
274
                break;
275
            case Util\Platform::OS_OSX:
276
                // Typora on macOS
277
                if ($editor == 'typora') {
278
                    $command = \sprintf('open -a typora "%s"', $path);
279
                }
280
                break;
281
        }
282
        $process = Process::fromShellCommandline($command);
283
        $process->run();
284
        if (!$process->isSuccessful()) {
285
            throw new RuntimeException(\sprintf('Unable to use "%s" editor.', $editor));
286
        }
287
    }
288
289
    /**
290
     * Validate URL.
291
     *
292
     * @throws RuntimeException
293
     */
294
    public static function validateUrl(string $url): string
295
    {
296
        if ($url == '/') { // tolerate root URL
297
            return $url;
298
        }
299
        $validator = Validation::createValidator();
300
        $violations = $validator->validate($url, new Url());
301
        if (\count($violations) > 0) {
302
            foreach ($violations as $violation) {
303
                throw new RuntimeException($violation->getMessage());
304
            }
305
        }
306
        return rtrim($url, '/') . '/';
307
    }
308
309
    /**
310
     * Returns the "binary name" in the console context.
311
     */
312
    protected function binName(): string
313
    {
314
        return basename($_SERVER['argv'][0]);
315
    }
316
317
    /**
318
     * Override default help message.
319
     *
320
     * @return string
321
     */
322
    public function getProcessedHelp(): string
323
    {
324
        $name = $this->getName();
325
        $placeholders = [
326
            '%command.name%',
327
            '%command.full_name%',
328
        ];
329
        $replacements = [
330
            $name,
331
            $this->binName() . ' ' . $name,
332
        ];
333
334
        return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
335
    }
336
}
337