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

Cecilapp / Cecil / 12021297559

26 Nov 2024 12:25AM UTC coverage: 83.555% (-0.2%) from 83.781%
12021297559

push

github

web-flow
refactor: rebuild configuration (#2068)

63 of 85 new or added lines in 11 files covered. (74.12%)

6 existing lines in 1 file now uncovered.

2957 of 3539 relevant lines covered (83.55%)

0.84 hits per line

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

76.61
/src/Config.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <arnaud@ligny.fr>
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;
15

16
use Cecil\Exception\ConfigException;
17
use Cecil\Util\Plateform;
18
use Dflydev\DotAccessData\Data;
19

20
/**
21
 * Class Config.
22
 */
23
class Config
24
{
25
    /** Configuration is a Data object. */
26
    protected Data $data;
27

28
    /** Default configuration is a Data object. */
29
    protected Data $default;
30

31
    /** Source directory. */
32
    protected string $sourceDir;
33

34
    /** Destination directory. */
35
    protected string $destinationDir;
36

37
    /** Languages list as array. */
38
    protected ?array $languages = null;
39

40
    public const PRESERVE = 0;
41
    public const REPLACE = 1;
42
    public const MERGE = 2;
43
    public const LANG_CODE_PATTERN = '([a-z]{2}(-[A-Z]{2})?)'; // "fr" or "fr-FR"
44
    public const LANG_LOCALE_PATTERN = '[a-z]{2}(_[A-Z]{2})?(_[A-Z]{2})?'; // "fr" or "fr_FR" or "no_NO_NY"
45

46
    /**
47
     * Build the Config object with the default config + the optional given array.
48
     */
49
    public function __construct(?array $config = null)
50
    {
51
        // default configuration
52
        $defaultConfigFile = realpath(Util::joinFile(__DIR__, '..', 'config/default.php'));
1✔
53
        if (Plateform::isPhar()) {
1✔
NEW
54
            $defaultConfigFile = Util::joinPath(Plateform::getPharPath(), 'config/default.php');
×
55
        }
56
        $this->default = new Data(include $defaultConfigFile);
1✔
57

58
        // base configuration
59
        $baseConfigFile = realpath(Util::joinFile(__DIR__, '..', 'config/base.php'));
1✔
60
        if (Plateform::isPhar()) {
1✔
NEW
61
            $baseConfigFile = Util::joinPath(Plateform::getPharPath(), 'config/base.php');
×
62
        }
63
        $this->data = new Data(include $baseConfigFile);
1✔
64

65
        // import config
66
        $this->import($config ?? []);
1✔
67
    }
68

69
    /**
70
     * Imports (and validate) configuration.
71
     */
72
    public function import(array $config, $mode = self::MERGE): void
73
    {
74
        $this->data->import($config, $mode);
1✔
75
        $this->validate();
1✔
76
        $this->override();
1✔
77
    }
78

79
    /**
80
     * Get configuration as an array.
81
     */
82
    public function getAsArray(): array
83
    {
84
        return $this->data->export();
×
85
    }
86

87
    /**
88
     * Is configuration's key exists?
89
     *
90
     * @param string $key      Configuration key
91
     * @param string $language Language code (optional)
92
     * @param bool   $fallback Set to false to not return the value in the default language as fallback
93
     */
94
    public function has(string $key, ?string $language = null, bool $fallback = true): bool
95
    {
96
        $default = $this->default->has($key);
1✔
97

98
        if ($language !== null) {
1✔
NEW
99
            $langIndex = $this->getLanguageIndex($language);
×
NEW
100
            $keyLang = "languages.$langIndex.config.$key";
×
NEW
101
            if ($this->data->has($keyLang)) {
×
NEW
102
                return true;
×
103
            }
NEW
104
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
×
NEW
105
                return $default;
×
106
            }
107
        }
108
        if ($this->data->has($key)) {
1✔
109
            return true;
1✔
110
        }
111

112
        return $default;
1✔
113
    }
114

115
    /**
116
     * Get the value of a configuration's key.
117
     *
118
     * @param string $key      Configuration key
119
     * @param string $language Language code (optional)
120
     * @param bool   $fallback Set to false to not return the value in the default language as fallback
121
     *
122
     * @return mixed|null
123
     */
124
    public function get(string $key, ?string $language = null, bool $fallback = true)
125
    {
126
        $default = $this->default->has($key) ? $this->default->get($key) : null;
1✔
127

128
        if ($language !== null) {
1✔
129
            $langIndex = $this->getLanguageIndex($language);
1✔
130
            $keyLang = "languages.$langIndex.config.$key";
1✔
131
            if ($this->data->has($keyLang)) {
1✔
132
                return $this->data->get($keyLang);
1✔
133
            }
134
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
1✔
135
                return $default;
1✔
136
            }
137
        }
138
        if ($this->data->has($key)) {
1✔
139
            return $this->data->get($key);
1✔
140
        }
141

142
        return $default;
1✔
143
    }
144

145
    /**
146
     * Set the source directory.
147
     *
148
     * @throws \InvalidArgumentException
149
     */
150
    public function setSourceDir(?string $sourceDir = null): self
151
    {
152
        if ($sourceDir === null) {
1✔
153
            $sourceDir = getcwd();
1✔
154
        }
155
        if (!is_dir($sourceDir)) {
1✔
156
            throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid source.', $sourceDir));
×
157
        }
158
        $this->sourceDir = $sourceDir;
1✔
159

160
        return $this;
1✔
161
    }
162

163
    /**
164
     * Get the source directory.
165
     */
166
    public function getSourceDir(): string
167
    {
168
        return $this->sourceDir;
1✔
169
    }
170

171
    /**
172
     * Set the destination directory.
173
     *
174
     * @throws \InvalidArgumentException
175
     */
176
    public function setDestinationDir(?string $destinationDir = null): self
177
    {
178
        if ($destinationDir === null) {
1✔
179
            $destinationDir = $this->sourceDir;
1✔
180
        }
181
        if (!is_dir($destinationDir)) {
1✔
182
            throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid destination.', $destinationDir));
×
183
        }
184
        $this->destinationDir = $destinationDir;
1✔
185

186
        return $this;
1✔
187
    }
188

189
    /**
190
     * Get the destination directory.
191
     */
192
    public function getDestinationDir(): string
193
    {
194
        return $this->destinationDir;
1✔
195
    }
196

197
    /*
198
     * Path helpers.
199
     */
200

201
    /**
202
     * Returns the path of the pages directory.
203
     */
204
    public function getPagesPath(): string
205
    {
206
        return Util::joinFile($this->getSourceDir(), (string) $this->get('pages.dir'));
1✔
207
    }
208

209
    /**
210
     * Returns the path of the output directory.
211
     */
212
    public function getOutputPath(): string
213
    {
214
        return Util::joinFile($this->getDestinationDir(), (string) $this->get('output.dir'));
1✔
215
    }
216

217
    /**
218
     * Returns the path of the data directory.
219
     */
220
    public function getDataPath(): string
221
    {
222
        return Util::joinFile($this->getSourceDir(), (string) $this->get('data.dir'));
1✔
223
    }
224

225
    /**
226
     * Returns the path of templates directory.
227
     */
228
    public function getLayoutsPath(): string
229
    {
230
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.dir'));
1✔
231
    }
232

233
    /**
234
     * Returns the path of internal templates directory.
235
     */
236
    public function getLayoutsInternalPath(): string
237
    {
238
        return Util::joinPath(__DIR__, '..', (string) $this->get('layouts.internal.dir'));
1✔
239
    }
240

241
    /**
242
     * Returns the path of translations directory.
243
     */
244
    public function getTranslationsPath(): string
245
    {
246
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.translations.dir'));
1✔
247
    }
248

249
    /**
250
     * Returns the path of internal translations directory.
251
     */
252
    public function getTranslationsInternalPath(): string
253
    {
254
        if (Util\Plateform::isPhar()) {
1✔
255
            return Util::joinPath(Plateform::getPharPath(), (string) $this->get('layouts.translations.internal.dir'));
×
256
        }
257

258
        return realpath(Util::joinPath(__DIR__, '..', (string) $this->get('layouts.translations.internal.dir')));
1✔
259
    }
260

261
    /**
262
     * Returns the path of themes directory.
263
     */
264
    public function getThemesPath(): string
265
    {
266
        return Util::joinFile($this->getSourceDir(), (string) $this->get('themes.dir'));
1✔
267
    }
268

269
    /**
270
     * Returns the path of static files directory.
271
     */
272
    public function getStaticPath(): string
273
    {
274
        return Util::joinFile($this->getSourceDir(), (string) $this->get('static.dir'));
1✔
275
    }
276

277
    /**
278
     * Returns the path of static files directory, with a target.
279
     */
280
    public function getStaticTargetPath(): string
281
    {
282
        $path = $this->getStaticPath();
1✔
283

284
        if (!empty($this->get('static.target'))) {
1✔
285
            $path = substr($path, 0, -\strlen((string) $this->get('static.target')));
×
286
        }
287

288
        return $path;
1✔
289
    }
290

291
    /**
292
     * Returns the path of assets files directory.
293
     */
294
    public function getAssetsPath(): string
295
    {
296
        return Util::joinFile($this->getSourceDir(), (string) $this->get('assets.dir'));
1✔
297
    }
298

299
    /**
300
     * Returns cache path.
301
     *
302
     * @throws ConfigException
303
     */
304
    public function getCachePath(): string
305
    {
306
        if (empty((string) $this->get('cache.dir'))) {
1✔
NEW
307
            throw new ConfigException(\sprintf('The cache directory ("%s") is not defined.', 'cache.dir'));
×
308
        }
309

310
        if ($this->isCacheDirIsAbsolute()) {
1✔
311
            $cacheDir = Util::joinFile((string) $this->get('cache.dir'), 'cecil');
×
312
            Util\File::getFS()->mkdir($cacheDir);
×
313

314
            return $cacheDir;
×
315
        }
316

317
        return Util::joinFile($this->getDestinationDir(), (string) $this->get('cache.dir'));
1✔
318
    }
319

320
    /**
321
     * Returns cache path of templates.
322
     */
323
    public function getCacheTemplatesPath(): string
324
    {
325
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.templates.dir'));
1✔
326
    }
327

328
    /**
329
     * Returns cache path of translations.
330
     */
331
    public function getCacheTranslationsPath(): string
332
    {
333
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.translations.dir'));
1✔
334
    }
335

336
    /**
337
     * Returns cache path of assets.
338
     */
339
    public function getCacheAssetsPath(): string
340
    {
341
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.assets.dir'));
1✔
342
    }
343

344
    /**
345
     * Returns cache path of remote assets.
346
     */
347
    public function getCacheAssetsRemotePath(): string
348
    {
349
        return Util::joinFile($this->getCacheAssetsPath(), (string) $this->get('cache.assets.remote.dir'));
1✔
350
    }
351

352
    /*
353
     * Output helpers.
354
     */
355

356
    /**
357
     * Returns the property value of an output format.
358
     *
359
     * @throws ConfigException
360
     */
361
    public function getOutputFormatProperty(string $name, string $property): string|array|null
362
    {
363
        $properties = array_column((array) $this->get('output.formats'), $property, 'name');
1✔
364

365
        if (empty($properties)) {
1✔
NEW
366
            throw new ConfigException(\sprintf('Property "%s" is not defined for format "%s".', $property, $name));
×
367
        }
368

369
        return $properties[$name] ?? null;
1✔
370
    }
371

372
    /*
373
     * Assets helpers.
374
     */
375

376
    /**
377
     * Returns asset image widths.
378
     */
379
    public function getAssetsImagesWidths(): array
380
    {
381
        return $this->get('assets.images.responsive.widths');
1✔
382
    }
383

384
    /**
385
     * Returns asset image sizes.
386
     */
387
    public function getAssetsImagesSizes(): array
388
    {
389
        return $this->get('assets.images.responsive.sizes');
1✔
390
    }
391

392
    /*
393
     * Theme helpers.
394
     */
395

396
    /**
397
     * Returns theme(s) as an array.
398
     */
399
    public function getTheme(): ?array
400
    {
401
        if ($themes = $this->get('theme')) {
1✔
402
            if (\is_array($themes)) {
1✔
403
                return $themes;
1✔
404
            }
405

406
            return [$themes];
×
407
        }
408

409
        return null;
×
410
    }
411

412
    /**
413
     * Has a (valid) theme(s)?
414
     *
415
     * @throws ConfigException
416
     */
417
    public function hasTheme(): bool
418
    {
419
        if ($themes = $this->getTheme()) {
1✔
420
            foreach ($themes as $theme) {
1✔
421
                if (!Util\File::getFS()->exists($this->getThemeDirPath($theme, 'layouts')) && !Util\File::getFS()->exists(Util::joinFile($this->getThemesPath(), $theme, 'config.yml'))) {
1✔
NEW
422
                    throw new ConfigException(\sprintf('Theme "%s" not found. Did you forgot to install it?', $theme));
×
423
                }
424
            }
425

426
            return true;
1✔
427
        }
428

429
        return false;
×
430
    }
431

432
    /**
433
     * Returns the path of a specific theme's directory.
434
     * ("layouts" by default).
435
     */
436
    public function getThemeDirPath(string $theme, string $dir = 'layouts'): string
437
    {
438
        return Util::joinFile($this->getThemesPath(), $theme, $dir);
1✔
439
    }
440

441
    /*
442
     * Language helpers.
443
     */
444

445
    /**
446
     * Returns an array of available languages.
447
     *
448
     * @throws ConfigException
449
     */
450
    public function getLanguages(): array
451
    {
452
        if ($this->languages !== null) {
1✔
453
            return $this->languages;
1✔
454
        }
455

456
        $languages = array_filter((array) $this->get('languages'), function ($language) {
1✔
457
            return !(isset($language['enabled']) && $language['enabled'] === false);
1✔
458
        });
1✔
459

460
        if (!\is_int(array_search($this->getLanguageDefault(), array_column($languages, 'code')))) {
1✔
NEW
461
            throw new ConfigException(\sprintf('The default language "%s" is not listed in "languages".', $this->getLanguageDefault()));
×
462
        }
463

464
        $this->languages = $languages;
1✔
465

466
        return $this->languages;
1✔
467
    }
468

469
    /**
470
     * Returns the default language code (ie: "en", "fr-FR", etc.).
471
     *
472
     * @throws ConfigException
473
     */
474
    public function getLanguageDefault(): string
475
    {
476
        if (!$this->get('language')) {
1✔
NEW
477
            throw new ConfigException('There is no default "language" key.');
×
478
        }
479
        if (\is_array($this->get('language'))) {
1✔
480
            if (!$this->get('language.code')) {
×
NEW
481
                throw new ConfigException('There is no "language.code" key.');
×
482
            }
483

484
            return $this->get('language.code');
×
485
        }
486

487
        return $this->get('language');
1✔
488
    }
489

490
    /**
491
     * Returns a language code index.
492
     *
493
     * @throws ConfigException
494
     */
495
    public function getLanguageIndex(string $code): int
496
    {
497
        $array = array_column($this->getLanguages(), 'code');
1✔
498

499
        if (false === $index = array_search($code, $array)) {
1✔
NEW
500
            throw new ConfigException(\sprintf('The language code "%s" is not defined.', $code));
×
501
        }
502

503
        return $index;
1✔
504
    }
505

506
    /**
507
     * Returns the property value of a (specified or the default) language.
508
     *
509
     * @throws ConfigException
510
     */
511
    public function getLanguageProperty(string $property, ?string $code = null): string
512
    {
513
        $code = $code ?? $this->getLanguageDefault();
1✔
514

515
        $properties = array_column($this->getLanguages(), $property, 'code');
1✔
516

517
        if (empty($properties)) {
1✔
NEW
518
            throw new ConfigException(\sprintf('Property "%s" is not defined for language "%s".', $property, $code));
×
519
        }
520

521
        return $properties[$code];
1✔
522
    }
523

524
    /*
525
     * Cache helpers.
526
     */
527

528
    /**
529
     * Is cache dir is absolute to system files
530
     * or relative to project destination?
531
     */
532
    public function isCacheDirIsAbsolute(): bool
533
    {
534
        $path = (string) $this->get('cache.dir');
1✔
535
        if (Util::joinFile($path) == realpath(Util::joinFile($path))) {
1✔
536
            return true;
×
537
        }
538

539
        return false;
1✔
540
    }
541

542
    /**
543
     * Overrides configuration with environment variables.
544
     */
545
    private function override(): void
546
    {
547
        $data = $this->data;
1✔
548
        $applyEnv = function ($array) use ($data) {
1✔
549
            $iterator = new \RecursiveIteratorIterator(
1✔
550
                new \RecursiveArrayIterator($array),
1✔
551
                \RecursiveIteratorIterator::SELF_FIRST
1✔
552
            );
1✔
553
            $iterator->rewind();
1✔
554
            while ($iterator->valid()) {
1✔
555
                $path = [];
1✔
556
                foreach (range(0, $iterator->getDepth()) as $depth) {
1✔
557
                    $path[] = $iterator->getSubIterator($depth)->key();
1✔
558
                }
559
                $sPath = implode('_', $path);
1✔
560
                if ($getEnv = getenv('CECIL_' . strtoupper($sPath))) {
1✔
561
                    $data->set(str_replace('_', '.', strtolower($sPath)), $this->castSetValue($getEnv));
1✔
562
                }
563
                $iterator->next();
1✔
564
            }
565
        };
1✔
566
        $applyEnv($data->export());
1✔
567
    }
568

569
    /**
570
     * Casts boolean value given to set() as string.
571
     *
572
     * @param mixed $value
573
     *
574
     * @return bool|mixed
575
     */
576
    private function castSetValue($value)
577
    {
578
        $filteredValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1✔
579

580
        if ($filteredValue !== null) {
1✔
581
            return $filteredValue;
1✔
582
        }
583

584
        return $value;
1✔
585
    }
586

587
    /**
588
     * Validate the configuration.
589
     *
590
     * @throws ConfigException
591
     */
592
    private function validate(): void
593
    {
594
        // default language must be valid
595
        if (!preg_match('/^' . Config::LANG_CODE_PATTERN . '$/', $this->getLanguageDefault())) {
1✔
596
            throw new ConfigException(\sprintf('Default language code "%s" is not valid (e.g.: "language: fr-FR").', $this->getLanguageDefault()));
×
597
        }
598
        // if language is set then the locale is required and must be valid
599
        foreach ((array) $this->get('languages') as $lang) {
1✔
600
            if (!isset($lang['locale'])) {
1✔
601
                throw new ConfigException('A language locale is not defined.');
×
602
            }
603
            if (!preg_match('/^' . Config::LANG_LOCALE_PATTERN . '$/', $lang['locale'])) {
1✔
604
                throw new ConfigException(\sprintf('The language locale "%s" is not valid (e.g.: "locale: fr_FR").', $lang['locale']));
×
605
            }
606
        }
607

608
        // version 8.x breaking changes detection
609
        $toV8 = [
1✔
610
            'frontmatter'  => 'pages:frontmatter',
1✔
611
            'body'         => 'pages:body',
1✔
612
            'defaultpages' => 'pages:default',
1✔
613
            'virtualpages' => 'pages:virtual',
1✔
614
            'generators'   => 'pages:generators',
1✔
615
            'translations' => 'layouts:translations',
1✔
616
            'extensions'   => 'layouts:extensions',
1✔
617
            'postprocess'  => 'optimize',
1✔
618
        ];
1✔
619
        array_walk($toV8, function ($to, $from) {
1✔
620
            if ($this->has($from)) {
1✔
621
                $path = explode(':', $to);
×
622
                $step = 0;
×
623
                $formatedPath = '';
×
624
                foreach ($path as $fragment) {
×
625
                    $step = $step + 2;
×
626
                    $formatedPath .= "$fragment:\n" . str_pad(' ', $step);
×
627
                }
628
                throw new ConfigException("Option `$from:` must be moved to:\n```\n$formatedPath\n```");
×
629
            }
630
        });
1✔
631
    }
632
}
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