Passed
Push — analysis-nerGoy ( 891d38 )
by Arnaud
10:02 queued 04:59
created

AbstractCommand::getBuilder()   C

Complexity

Conditions 12
Paths 53

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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