Passed
Push — master ( 3557d5...3616ca )
by Arnaud
12:29 queued 06:52
created

AbstractCommand::openEditor()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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