Passed
Push — cmd ( 8f0602...12f0fc )
by Arnaud
13:59 queued 09:23
created

AbstractCommand::getProcessedHelp()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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