Passed
Push — nested-sections ( 2d40b2...7dbf34 )
by Arnaud
12:03 queued 05:37
created

AbstractCommand   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 99
dl 0
loc 227
rs 9.0399
c 0
b 0
f 0
wmc 42

8 Methods

Rating   Name   Duplication   Size   Complexity  
B getPath() 0 25 7
A run() 0 26 6
B initialize() 0 30 9
A findConfigFile() 0 16 3
A getConfigFiles() 0 3 1
A validateUrl() 0 10 3
B getBuilder() 0 27 8
A openEditor() 0 18 5

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\Exception\RuntimeException;
18
use Cecil\Logger\ConsoleLogger;
19
use Cecil\Util;
20
use Symfony\Component\Console\Command\Command;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use Symfony\Component\Console\Style\SymfonyStyle;
24
use Symfony\Component\Filesystem\Path;
25
use Symfony\Component\Process\Process;
26
use Symfony\Component\Validator\Constraints\Url;
27
use Symfony\Component\Validator\Validation;
28
use Symfony\Component\Yaml\Exception\ParseException;
29
use Symfony\Component\Yaml\Yaml;
30
31
class AbstractCommand extends Command
32
{
33
    public const CONFIG_FILE = ['cecil.yml', 'config.yml'];
34
    public const TMP_DIR = '.cecil';
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 array */
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
        // set up configuration
67
        if (!\in_array($this->getName(), ['new:site', 'self-update'])) {
68
            // default configuration file
69
            $this->configFiles[$this->findConfigFile('name')] = $this->findConfigFile('path');
70
            // from --config=<file>
71
            if ($input->hasOption('config') && $input->getOption('config') !== null) {
72
                foreach (explode(',', (string) $input->getOption('config')) as $configFile) {
73
                    $this->configFiles[$configFile] = realpath($configFile);
74
                    if (!Util\File::getFS()->isAbsolutePath($configFile)) {
75
                        $this->configFiles[$configFile] = realpath(Util::joinFile($this->getPath(), $configFile));
76
                    }
77
                }
78
                $this->configFiles = array_unique($this->configFiles);
79
            }
80
            // checks file(s)
81
            foreach ($this->configFiles as $fileName => $filePath) {
82
                if ($filePath === false || !file_exists($filePath)) {
83
                    unset($this->configFiles[$fileName]);
84
                    $this->getBuilder()->getLogger()->error(sprintf('Could not find configuration file "%s".', $fileName));
85
                }
86
            }
87
        }
88
89
        parent::initialize($input, $output);
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95
    public function run(InputInterface $input, OutputInterface $output): int
96
    {
97
        // disable debug mode if a verbosity level is specified
98
        if ($output->getVerbosity() != OutputInterface::VERBOSITY_NORMAL) {
99
            putenv('CECIL_DEBUG=false');
100
        }
101
        // force verbosity level to "debug" in debug mode
102
        if (getenv('CECIL_DEBUG') == 'true') {
103
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
104
        }
105
        if ($output->isDebug()) {
106
            // set env. variable in debug mode
107
            putenv('CECIL_DEBUG=true');
108
109
            return parent::run($input, $output);
110
        }
111
        // simplified error message
112
        try {
113
            return parent::run($input, $output);
114
        } catch (\Exception $e) {
115
            if ($this->io === null) {
116
                $this->io = new SymfonyStyle($input, $output);
117
            }
118
            $this->io->error($e->getMessage());
119
120
            exit(1);
121
        }
122
    }
123
124
    /**
125
     * Returns the working path.
126
     */
127
    protected function getPath(bool $exist = true): ?string
128
    {
129
        if ($this->path === null) {
130
            try {
131
                // get working directory by default
132
                if (false === $this->path = getcwd()) {
133
                    throw new \Exception('Can\'t get current working directory.');
134
                }
135
                // ... or path
136
                if ($this->input->getArgument('path') !== null) {
137
                    $this->path = Path::canonicalize($this->input->getArgument('path'));
138
                }
139
                // try to get canonicalized absolute path
140
                if ($exist) {
141
                    if (realpath($this->path) === false) {
142
                        throw new \Exception(sprintf('The given path "%s" is not valid.', $this->path));
143
                    }
144
                    $this->path = realpath($this->path);
145
                }
146
            } catch (\Exception $e) {
147
                throw new \Exception($e->getMessage());
148
            }
149
        }
150
151
        return $this->path;
152
    }
153
154
    /**
155
     * Returns the configuration file name or path, if file exists, otherwise default name or false.
156
     */
157
    protected function findConfigFile(string $nameOrPath): string|false
158
    {
159
        $config = [
160
            'name' => self::CONFIG_FILE[0],
161
            'path' => false,
162
        ];
163
        foreach (self::CONFIG_FILE as $configFileName) {
164
            if (($configFilePath = realpath(Util::joinFile($this->getPath(), $configFileName))) !== false) {
165
                $config = [
166
                    'name' => $configFileName,
167
                    'path' => $configFilePath,
168
                ];
169
            }
170
        }
171
172
        return $config[$nameOrPath];
173
    }
174
175
    /**
176
     * Returns config file(s) path.
177
     */
178
    protected function getConfigFiles(): ?array
179
    {
180
        return $this->configFiles;
181
    }
182
183
    /**
184
     * Creates or returns a Builder instance.
185
     *
186
     * @throws RuntimeException
187
     */
188
    protected function getBuilder(array $config = []): Builder
189
    {
190
        try {
191
            // config
192
            if ($this->config === null) {
193
                $siteConfig = [];
194
                foreach ($this->getConfigFiles() as $fileName => $filePath) {
195
                    if ($filePath === false || false === $configContent = Util\File::fileGetContents($filePath)) {
196
                        throw new RuntimeException(sprintf('Can\'t read configuration file "%s".', $fileName));
197
                    }
198
                    $siteConfig = array_replace_recursive($siteConfig, (array) Yaml::parse($configContent, Yaml::PARSE_DATETIME));
199
                }
200
                $this->config = array_replace_recursive($siteConfig, $config);
201
            }
202
            // builder
203
            if ($this->builder === null) {
204
                $this->builder = (new Builder($this->config, new ConsoleLogger($this->output)))
205
                    ->setSourceDir($this->getPath())
206
                    ->setDestinationDir($this->getPath());
207
            }
208
        } catch (ParseException $e) {
209
            throw new RuntimeException(sprintf('Configuration parsing error: %s', $e->getMessage()));
210
        } catch (\Exception $e) {
211
            throw new RuntimeException($e->getMessage());
212
        }
213
214
        return $this->builder;
215
    }
216
217
    /**
218
     * Opens path with editor.
219
     *
220
     * @throws RuntimeException
221
     */
222
    protected function openEditor(string $path, string $editor): void
223
    {
224
        $command = sprintf('%s "%s"', $editor, $path);
225
        switch (Util\Plateform::getOS()) {
226
            case Util\Plateform::OS_WIN:
227
                $command = sprintf('start /B "" %s "%s"', $editor, $path);
228
                break;
229
            case Util\Plateform::OS_OSX:
230
                // Typora on macOS
231
                if ($editor == 'typora') {
232
                    $command = sprintf('open -a typora "%s"', $path);
233
                }
234
                break;
235
        }
236
        $process = Process::fromShellCommandline($command);
237
        $process->run();
238
        if (!$process->isSuccessful()) {
239
            throw new RuntimeException(sprintf('Can\'t use "%s" editor.', $editor));
240
        }
241
    }
242
243
    /**
244
     * Validate URL.
245
     *
246
     * @throws RuntimeException
247
     */
248
    public static function validateUrl(string $url): string
249
    {
250
        $validator = Validation::createValidator();
251
        $violations = $validator->validate($url, new Url());
252
        if (\count($violations) > 0) {
253
            foreach ($violations as $violation) {
254
                throw new RuntimeException($violation->getMessage());
255
            }
256
        }
257
        return rtrim($url, '/') . '/';
258
    }
259
}
260