Passed
Pull Request — master (#2148)
by Arnaud
11:21 queued 04:41
created

AbstractCommand::getPath()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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