• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 18576584875

16 Oct 2025 10:30PM UTC coverage: 64.749%. First build
18576584875

Pull #2233

github

web-flow
Merge b088a0809 into bd3a52bf1
Pull Request #2233: Apply fixes from StyleCI

0 of 1 new or added line in 1 file covered. (0.0%)

3163 of 4885 relevant lines covered (64.75%)

0.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Command/AbstractCommand.php
1
<?php
2

3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <arnaud@ligny.fr>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace Cecil\Command;
15

16
use Cecil\Builder;
17
use Cecil\Config;
18
use Cecil\Exception\RuntimeException;
19
use Cecil\Logger\ConsoleLogger;
20
use Cecil\Util;
21
use Joli\JoliNotif\Notification;
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

31
/**
32
 * Abstract command class.
33
 *
34
 * This class provides common functionality for all commands, such as configuration loading, path handling, and error management.
35
 */
36
class AbstractCommand extends Command
37
{
38
    public const CONFIG_FILE = ['cecil.yml', 'config.yml'];
39
    public const TMP_DIR = '.cecil';
40
    public const EXCLUDED_CMD = ['about', 'new:site', 'self-update'];
41
    public const SERVE_OUTPUT = '.cecil/preview';
42
    public const DESKTOP_NOTIFICATION = false; // set to true to enable desktop notifications
43

44
    /** @var Notification */
45
    public $notification;
46

47
    /** @var InputInterface */
48
    protected $input;
49

50
    /** @var OutputInterface */
51
    protected $output;
52

53
    /** @var SymfonyStyle */
54
    protected $io;
55

56
    /** @var null|string */
57
    private $path = null;
58

59
    /** @var array */
60
    private $configFiles = [];
61

62
    /** @var Config */
63
    private $config;
64

65
    /** @var Builder */
66
    private $builder;
67

68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected function initialize(InputInterface $input, OutputInterface $output)
72
    {
73
        $this->input = $input;
×
74
        $this->output = $output;
×
75
        $this->io = new SymfonyStyle($input, $output);
×
76

77
        // prepare configuration files list
78
        if (!\in_array($this->getName(), self::EXCLUDED_CMD)) {
×
79
            // site config file
80
            $this->configFiles[$this->locateConfigFile($this->getPath())['name']] = $this->locateConfigFile($this->getPath())['path'];
×
81
            // additional config file(s) from --config=<file>
82
            if ($input->hasOption('config') && $input->getOption('config') !== null) {
×
83
                $this->configFiles += $this->locateAdditionalConfigFiles($this->getPath(), (string) $input->getOption('config'));
×
84
            }
85
            // checks file(s)
86
            $this->configFiles = array_unique($this->configFiles);
×
87
            foreach ($this->configFiles as $fileName => $filePath) {
×
88
                if ($filePath === false) {
×
89
                    unset($this->configFiles[$fileName]);
×
90
                    $this->io->warning(\sprintf('Could not find configuration file "%s".', $fileName));
×
91
                }
92
            }
93
        }
94
        // prepare notification
95
        if (self::DESKTOP_NOTIFICATION) {
×
96
            $this->notification = (new Notification())
×
97
                ->setTitle('Cecil')
×
NEW
98
                ->setIcon(__DIR__ . '/../../resources/icon.png')
×
99
                ->setBody('...')
×
100
            ;
×
101
        }
102

103
        parent::initialize($input, $output);
×
104
    }
105

106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function run(InputInterface $input, OutputInterface $output): int
110
    {
111
        // disable debug mode if a verbosity level is specified
112
        if ($output->getVerbosity() != OutputInterface::VERBOSITY_NORMAL) {
×
113
            putenv('CECIL_DEBUG=false');
×
114
        }
115
        // force verbosity level to "debug" in debug mode
116
        if (getenv('CECIL_DEBUG') == 'true') {
×
117
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
×
118
        }
119
        if ($output->isDebug()) {
×
120
            // set env. variable in debug mode
121
            putenv('CECIL_DEBUG=true');
×
122

123
            return parent::run($input, $output);
×
124
        }
125
        // run with human error message
126
        try {
127
            return parent::run($input, $output);
×
128
        } catch (\Exception $e) {
×
129
            if ($this->io === null) {
×
130
                $this->io = new SymfonyStyle($input, $output);
×
131
            }
132
            $message = '';
×
133
            $i = 0;
×
134
            do {
135
                //if ($e instanceof \Twig\Error\RuntimeError) {
136
                //    continue;
137
                //}
138

139
                if ($i > 0) {
×
140
                    $message .= '└ ';
×
141
                }
142
                $message .= "{$e->getMessage()}\n";
×
143
                if ($e->getFile() && $e instanceof RuntimeException) {
×
144
                    $message .= \sprintf("→ %s%s\n", $e->getFile(), $e->getLine() ? ":{$e->getLine()}" : '');
×
145
                }
146
                $i++;
×
147
            } while ($e = $e->getPrevious());
×
148
            $this->io->error($message);
×
149

150
            exit(1);
×
151
        }
152
    }
153

154
    /**
155
     * Returns the working path.
156
     */
157
    protected function getPath(bool $exist = true): ?string
158
    {
159
        if ($this->path === null) {
×
160
            try {
161
                // get working directory by default
162
                if (false === $this->path = getcwd()) {
×
163
                    throw new \Exception('Can\'t get current working directory.');
×
164
                }
165
                // ... or path
166
                if ($this->input->getArgument('path') !== null) {
×
167
                    $this->path = Path::canonicalize($this->input->getArgument('path'));
×
168
                }
169
                // try to get canonicalized absolute path
170
                if ($exist) {
×
171
                    if (realpath($this->path) === false) {
×
172
                        throw new \Exception(\sprintf('The given path "%s" is not valid.', $this->path));
×
173
                    }
174
                    $this->path = realpath($this->path);
×
175
                }
176
            } catch (\Exception $e) {
×
177
                throw new \Exception($e->getMessage());
×
178
            }
179
        }
180

181
        return $this->path;
×
182
    }
183

184
    /**
185
     * Returns config file(s) path.
186
     */
187
    protected function getConfigFiles(): array
188
    {
189
        return $this->configFiles ?? [];
×
190
    }
191

192
    /**
193
     * Creates or returns a Builder instance.
194
     *
195
     * @throws RuntimeException
196
     */
197
    protected function getBuilder(array $config = []): Builder
198
    {
199
        try {
200
            // loads configuration files if not already done
201
            if ($this->config === null) {
×
202
                $this->config = new Config();
×
203
                // loads and merges configuration files
204
                foreach ($this->getConfigFiles() as $filePath) {
×
205
                    $this->config->import($this->config::loadFile($filePath), Config::IMPORT_MERGE);
×
206
                }
207
                // merges configuration from $config parameter
208
                $this->config->import($config, Config::IMPORT_MERGE);
×
209
            }
210
            // creates builder instance if not already done
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
            }
216
        } catch (\Exception $e) {
×
217
            throw new RuntimeException($e->getMessage());
×
218
        }
219

220
        return $this->builder;
×
221
    }
222

223
    /**
224
     * Locates the configuration in the given path, as an array of the file name and path, if file exists, otherwise default name and false.
225
     */
226
    protected function locateConfigFile(string $path): array
227
    {
228
        $config = [
×
229
            'name' => self::CONFIG_FILE[0],
×
230
            'path' => false,
×
231
        ];
×
232
        foreach (self::CONFIG_FILE as $configFileName) {
×
233
            if (($configFilePath = realpath(Util::joinFile($path, $configFileName))) !== false) {
×
234
                $config = [
×
235
                    'name' => $configFileName,
×
236
                    'path' => $configFilePath,
×
237
                ];
×
238
            }
239
        }
240

241
        return $config;
×
242
    }
243

244
    /**
245
     * Locates additional configuration file(s) from the given list of files, relative to the given path or absolute.
246
     */
247
    protected function locateAdditionalConfigFiles(string $path, string $configFilesList): array
248
    {
249
        $config = [];
×
250
        foreach (explode(',', $configFilesList) as $filename) {
×
251
            // absolute path
252
            $config[$filename] = realpath($filename);
×
253
            // relative path
254
            if (!Util\File::getFS()->isAbsolutePath($filename)) {
×
255
                $config[$filename] = realpath(Util::joinFile($path, $filename));
×
256
            }
257
        }
258

259
        return $config;
×
260
    }
261

262
    /**
263
     * Opens path with editor.
264
     *
265
     * @throws RuntimeException
266
     */
267
    protected function openEditor(string $path, string $editor): void
268
    {
269
        $command = \sprintf('%s "%s"', $editor, $path);
×
270
        switch (Util\Platform::getOS()) {
×
271
            case Util\Platform::OS_WIN:
×
272
                $command = \sprintf('start /B "" %s "%s"', $editor, $path);
×
273
                break;
×
274
            case Util\Platform::OS_OSX:
×
275
                // Typora on macOS
276
                if ($editor == 'typora') {
×
277
                    $command = \sprintf('open -a typora "%s"', $path);
×
278
                }
279
                break;
×
280
        }
281
        $process = Process::fromShellCommandline($command);
×
282
        $process->run();
×
283
        if (!$process->isSuccessful()) {
×
284
            throw new RuntimeException(\sprintf('Can\'t use "%s" editor.', $editor));
×
285
        }
286
    }
287

288
    /**
289
     * Validate URL.
290
     *
291
     * @throws RuntimeException
292
     */
293
    public static function validateUrl(string $url): string
294
    {
295
        if ($url == '/') { // tolerate root URL
×
296
            return $url;
×
297
        }
298
        $validator = Validation::createValidator();
×
299
        $violations = $validator->validate($url, new Url());
×
300
        if (\count($violations) > 0) {
×
301
            foreach ($violations as $violation) {
×
302
                throw new RuntimeException($violation->getMessage());
×
303
            }
304
        }
305
        return rtrim($url, '/') . '/';
×
306
    }
307

308
    /**
309
     * Returns the "binary name" in the console context.
310
     */
311
    protected function binName(): string
312
    {
313
        return basename($_SERVER['argv'][0]);
×
314
    }
315

316
    /**
317
     * Override default help message.
318
     *
319
     * @return string
320
     */
321
    public function getProcessedHelp(): string
322
    {
323
        $name = $this->getName();
×
324
        $placeholders = [
×
325
            '%command.name%',
×
326
            '%command.full_name%',
×
327
        ];
×
328
        $replacements = [
×
329
            $name,
×
330
            $this->binName() . ' ' . $name,
×
331
        ];
×
332

333
        return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
×
334
    }
335
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc