Passed
Push — master ( 55e534...2ba0ad )
by
unknown
05:29
created

AbstractCommand   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 285
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 120
c 3
b 1
f 0
dl 0
loc 285
rs 8.5599
wmc 48

11 Methods

Rating   Name   Duplication   Size   Complexity  
A openEditor() 0 18 5
A initialize() 0 25 6
B getPath() 0 25 7
A locateAdditionalConfigFiles() 0 13 3
A locateConfigFile() 0 16 3
A getConfigFiles() 0 3 1
A getBuilder() 0 24 5
A getProcessedHelp() 0 13 2
A validateUrl() 0 13 4
A binName() 0 3 1
B run() 0 42 11

How to fix   Complexity   

Complex Class

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