Passed
Pull Request — master (#2148)
by Arnaud
09:07 queued 03:58
created

AbstractCommand::locateAdditionalConfigFiles()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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