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

Cecilapp / Cecil / 7142468876

08 Dec 2023 02:20PM UTC coverage: 83.0% (+0.5%) from 82.534%
7142468876

Pull #1676

github

web-flow
Merge 992f2274c into 814daa587
Pull Request #1676: 8.x dev

186 of 231 new or added lines in 31 files covered. (80.52%)

17 existing lines in 6 files now uncovered.

2861 of 3447 relevant lines covered (83.0%)

0.83 hits per line

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

76.51
/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\Exception\RuntimeException;
18
use Cecil\Util\Plateform;
19
use Dflydev\DotAccessData\Data;
20

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

29
    /** @var array Configuration. */
30
    protected $siteConfig;
31

32
    /** @var string Source directory. */
33
    protected $sourceDir;
34

35
    /** @var string Destination directory. */
36
    protected $destinationDir;
37

38
    /** @var array Languages. */
39
    protected $languages = null;
40

41
    public const LANG_CODE_PATTERN = '([a-z]{2}(-[A-Z]{2})?)'; // "fr" or "fr-FR"
42
    public const LANG_LOCALE_PATTERN = '[a-z]{2}(_[A-Z]{2})?(_[A-Z]{2})?'; // "fr" or "fr_FR" or "no_NO_NY"
43

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

56
        // import site config
57
        $this->siteConfig = $config;
1✔
58
        $this->importSiteConfig();
1✔
59
    }
60

61
    /**
62
     * Imports site configuration.
63
     */
64
    private function importSiteConfig(): void
65
    {
66
        $this->data->import($this->siteConfig, Data::REPLACE);
1✔
67

68
        /**
69
         * Overrides configuration with environment variables.
70
         */
71
        $data = $this->getData();
1✔
72
        $applyEnv = function ($array) use ($data) {
1✔
73
            $iterator = new \RecursiveIteratorIterator(
1✔
74
                new \RecursiveArrayIterator($array),
1✔
75
                \RecursiveIteratorIterator::SELF_FIRST
1✔
76
            );
1✔
77
            $iterator->rewind();
1✔
78
            while ($iterator->valid()) {
1✔
79
                $path = [];
1✔
80
                foreach (range(0, $iterator->getDepth()) as $depth) {
1✔
81
                    $path[] = $iterator->getSubIterator($depth)->key();
1✔
82
                }
83
                $sPath = implode('_', $path);
1✔
84
                if ($getEnv = getenv('CECIL_' . strtoupper($sPath))) {
1✔
85
                    $data->set(str_replace('_', '.', strtolower($sPath)), $this->castSetValue($getEnv));
1✔
86
                }
87
                $iterator->next();
1✔
88
            }
89
        };
1✔
90
        $applyEnv($data->export());
1✔
91
    }
92

93
    /**
94
     * Casts boolean value given to set() as string.
95
     *
96
     * @param mixed $value
97
     *
98
     * @return bool|mixed
99
     */
100
    private function castSetValue($value)
101
    {
102
        if (\is_string($value)) {
1✔
103
            switch ($value) {
104
                case 'true':
1✔
105
                    return true;
1✔
106
                case 'false':
1✔
107
                    return false;
×
108
                default:
109
                    return $value;
1✔
110
            }
111
        }
112

113
        return $value;
×
114
    }
115

116
    /**
117
     * Imports (theme) configuration.
118
     */
119
    public function import(array $config): void
120
    {
121
        $this->data->import($config, Data::REPLACE);
1✔
122

123
        // re-import site config
124
        $this->importSiteConfig();
1✔
125

126
        // checks the configuration
127
        $this->valid();
1✔
128
    }
129

130
    /**
131
     * Get configuration as an array.
132
     */
133
    public function getAsArray(): array
134
    {
135
        return $this->data->export();
×
136
    }
137

138
    /**
139
     * Is configuration's key exists?
140
     */
141
    public function has(string $key): bool
142
    {
143
        return $this->data->has($key);
1✔
144
    }
145

146
    /**
147
     * Get the value of a configuration's key.
148
     *
149
     * @param string $key      Configuration key
150
     * @param string $language Language code (optionnal)
151
     * @param bool   $fallback Set to false to not return the value in the default language as fallback
152
     *
153
     * @return mixed|null
154
     */
155
    public function get(string $key, ?string $language = null, bool $fallback = true)
156
    {
157
        if ($language !== null) {
1✔
158
            $langIndex = $this->getLanguageIndex($language);
1✔
159
            $keyLang = "languages.$langIndex.config.$key";
1✔
160
            if ($this->data->has($keyLang)) {
1✔
161
                return $this->data->get($keyLang);
1✔
162
            }
163
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
1✔
164
                return null;
1✔
165
            }
166
        }
167
        if ($this->data->has($key)) {
1✔
168
            return $this->data->get($key);
1✔
169
        }
170

171
        return null;
1✔
172
    }
173

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

189
        return $this;
1✔
190
    }
191

192
    /**
193
     * Get the source directory.
194
     */
195
    public function getSourceDir(): string
196
    {
197
        return $this->sourceDir;
1✔
198
    }
199

200
    /**
201
     * Set the destination directory.
202
     *
203
     * @throws \InvalidArgumentException
204
     */
205
    public function setDestinationDir(string $destinationDir = null): self
206
    {
207
        if ($destinationDir === null) {
1✔
208
            $destinationDir = $this->sourceDir;
1✔
209
        }
210
        if (!is_dir($destinationDir)) {
1✔
211
            throw new \InvalidArgumentException(sprintf(
×
212
                'The directory "%s" is not a valid destination!',
×
213
                $destinationDir
×
214
            ));
×
215
        }
216
        $this->destinationDir = $destinationDir;
1✔
217

218
        return $this;
1✔
219
    }
220

221
    /**
222
     * Get the destination directory.
223
     */
224
    public function getDestinationDir(): string
225
    {
226
        return $this->destinationDir;
1✔
227
    }
228

229
    /*
230
     * Path helpers.
231
     */
232

233
    /**
234
     * Returns the path of the pages directory.
235
     */
236
    public function getPagesPath(): string
237
    {
238
        return Util::joinFile($this->getSourceDir(), (string) $this->get('pages.dir'));
1✔
239
    }
240

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

249
    /**
250
     * Returns the path of the data directory.
251
     */
252
    public function getDataPath(): string
253
    {
254
        return Util::joinFile($this->getSourceDir(), (string) $this->get('data.dir'));
1✔
255
    }
256

257
    /**
258
     * Returns the path of templates directory.
259
     */
260
    public function getLayoutsPath(): string
261
    {
262
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.dir'));
1✔
263
    }
264

265
    /**
266
     * Returns the path of internal templates directory.
267
     */
268
    public function getLayoutsInternalPath(): string
269
    {
270
        return Util::joinPath(__DIR__, '..', (string) $this->get('layouts.internal.dir'));
1✔
271
    }
272

273
    /**
274
     * Returns the path of translations directory.
275
     */
276
    public function getTranslationsPath(): string
277
    {
278
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.translations.dir'));
1✔
279
    }
280

281
    /**
282
     * Returns the path of internal translations directory.
283
     */
284
    public function getTranslationsInternalPath(): string
285
    {
286
        if (Util\Plateform::isPhar()) {
1✔
NEW
287
            return Util::joinPath(Plateform::getPharPath(), (string) $this->get('layouts.translations.internal.dir'));
×
288
        }
289

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

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

301
    /**
302
     * Returns the path of static files directory.
303
     */
304
    public function getStaticPath(): string
305
    {
306
        return Util::joinFile($this->getSourceDir(), (string) $this->get('static.dir'));
1✔
307
    }
308

309
    /**
310
     * Returns the path of static files directory, with a target.
311
     */
312
    public function getStaticTargetPath(): string
313
    {
314
        $path = $this->getStaticPath();
1✔
315

316
        if (!empty($this->get('static.target'))) {
1✔
317
            $path = substr($path, 0, -\strlen((string) $this->get('static.target')));
×
318
        }
319

320
        return $path;
1✔
321
    }
322

323
    /**
324
     * Returns the path of assets files directory.
325
     */
326
    public function getAssetsPath(): string
327
    {
328
        return Util::joinFile($this->getSourceDir(), (string) $this->get('assets.dir'));
1✔
329
    }
330

331
    /**
332
     * Returns cache path.
333
     *
334
     * @throws RuntimeException
335
     */
336
    public function getCachePath(): string
337
    {
338
        if (empty((string) $this->get('cache.dir'))) {
1✔
339
            throw new RuntimeException(sprintf('The cache directory ("%s") is not defined in configuration.', 'cache.dir'));
×
340
        }
341

342
        if ($this->isCacheDirIsAbsolute()) {
1✔
343
            $cacheDir = Util::joinFile((string) $this->get('cache.dir'), 'cecil');
×
344
            Util\File::getFS()->mkdir($cacheDir);
×
345

346
            return $cacheDir;
×
347
        }
348

349
        return Util::joinFile($this->getDestinationDir(), (string) $this->get('cache.dir'));
1✔
350
    }
351

352
    /**
353
     * Returns cache path of templates.
354
     */
355
    public function getCacheTemplatesPath(): string
356
    {
357
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.templates.dir'));
1✔
358
    }
359

360
    /**
361
     * Returns cache path of translations.
362
     */
363
    public function getCacheTranslationsPath(): string
364
    {
365
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.translations.dir'));
1✔
366
    }
367

368
    /**
369
     * Returns cache path of assets.
370
     */
371
    public function getCacheAssetsPath(): string
372
    {
373
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.assets.dir'));
1✔
374
    }
375

376
    /**
377
     * Returns cache path of remote assets.
378
     */
379
    public function getCacheAssetsRemotePath(): string
380
    {
381
        return Util::joinFile($this->getCacheAssetsPath(), (string) $this->get('cache.assets.remote.dir'));
1✔
382
    }
383

384
    /*
385
     * Output helpers.
386
     */
387

388
    /**
389
     * Returns the property value of an output format.
390
     *
391
     * @throws RuntimeException
392
     *
393
     * @return string|array|null
394
     */
395
    public function getOutputFormatProperty(string $name, string $property)
396
    {
397
        $properties = array_column((array) $this->get('output.formats'), $property, 'name');
1✔
398

399
        if (empty($properties)) {
1✔
400
            throw new RuntimeException(sprintf('Property "%s" is not defined for format "%s".', $property, $name));
×
401
        }
402

403
        return $properties[$name] ?? null;
1✔
404
    }
405

406
    /*
407
     * Assets helpers.
408
     */
409

410
    /**
411
     * Returns asset image widths.
412
     */
413
    public function getAssetsImagesWidths(): array
414
    {
415
        return \count((array) $this->get('assets.images.responsive.widths')) > 0 ? (array) $this->get('assets.images.responsive.widths') : [480, 640, 768, 1024, 1366, 1600, 1920];
1✔
416
    }
417

418
    /**
419
     * Returns asset image sizes.
420
     */
421
    public function getAssetsImagesSizes(): array
422
    {
423
        return \count((array) $this->get('assets.images.responsive.sizes')) > 0 ? (array) $this->get('assets.images.responsive.sizes') : ['default' => '100vw'];
1✔
424
    }
425

426
    /*
427
     * Theme helpers.
428
     */
429

430
    /**
431
     * Returns theme(s) as an array.
432
     */
433
    public function getTheme(): ?array
434
    {
435
        if ($themes = $this->get('theme')) {
1✔
436
            if (\is_array($themes)) {
1✔
437
                return $themes;
1✔
438
            }
439

440
            return [$themes];
×
441
        }
442

443
        return null;
×
444
    }
445

446
    /**
447
     * Has a (valid) theme(s)?
448
     *
449
     * @throws RuntimeException
450
     */
451
    public function hasTheme(): bool
452
    {
453
        if ($themes = $this->getTheme()) {
1✔
454
            foreach ($themes as $theme) {
1✔
455
                if (!Util\File::getFS()->exists($this->getThemeDirPath($theme, 'layouts')) && !Util\File::getFS()->exists(Util::joinFile($this->getThemesPath(), $theme, 'config.yml'))) {
1✔
456
                    throw new RuntimeException(sprintf('Theme "%s" not found. Did you forgot to install it?', $theme));
×
457
                }
458
            }
459

460
            return true;
1✔
461
        }
462

463
        return false;
×
464
    }
465

466
    /**
467
     * Returns the path of a specific theme's directory.
468
     * ("layouts" by default).
469
     */
470
    public function getThemeDirPath(string $theme, string $dir = 'layouts'): string
471
    {
472
        return Util::joinFile($this->getThemesPath(), $theme, $dir);
1✔
473
    }
474

475
    /*
476
     * Language helpers.
477
     */
478

479
    /**
480
     * Returns an array of available languages.
481
     *
482
     * @throws RuntimeException
483
     */
484
    public function getLanguages(): array
485
    {
486
        if ($this->languages !== null) {
1✔
487
            return $this->languages;
1✔
488
        }
489

490
        $languages = array_filter((array) $this->get('languages'), function ($language) {
1✔
491
            return !(isset($language['enabled']) && $language['enabled'] === false);
1✔
492
        });
1✔
493

494
        if (!\is_int(array_search($this->getLanguageDefault(), array_column($languages, 'code')))) {
1✔
495
            throw new RuntimeException(sprintf('The default language "%s" is not listed in "languages" key configuration.', $this->getLanguageDefault()));
×
496
        }
497

498
        $this->languages = $languages;
1✔
499

500
        return $this->languages;
1✔
501
    }
502

503
    /**
504
     * Returns the default language code (ie: "en", "fr-FR", etc.).
505
     *
506
     * @throws RuntimeException
507
     */
508
    public function getLanguageDefault(): string
509
    {
510
        if (!$this->get('language')) {
1✔
511
            throw new RuntimeException('There is no default "language" key in configuration.');
×
512
        }
513

514
        if ($this->get('language.code')) {
1✔
515
            return $this->get('language.code');
×
516
        }
517

518
        return $this->get('language');
1✔
519
    }
520

521
    /**
522
     * Returns a language code index.
523
     *
524
     * @throws RuntimeException
525
     */
526
    public function getLanguageIndex(string $code): int
527
    {
528
        $array = array_column($this->getLanguages(), 'code');
1✔
529

530
        if (false === $index = array_search($code, $array)) {
1✔
531
            throw new RuntimeException(sprintf('The language code "%s" is not defined.', $code));
×
532
        }
533

534
        return $index;
1✔
535
    }
536

537
    /**
538
     * Returns the property value of a (specified or the default) language.
539
     *
540
     * @throws RuntimeException
541
     */
542
    public function getLanguageProperty(string $property, ?string $code = null): string
543
    {
544
        $code = $code ?? $this->getLanguageDefault();
1✔
545

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

548
        if (empty($properties)) {
1✔
549
            throw new RuntimeException(sprintf('Property "%s" is not defined for language "%s".', $property, $code));
×
550
        }
551

552
        return $properties[$code];
1✔
553
    }
554

555
    /*
556
     * Cache helpers.
557
     */
558

559
    /**
560
     * Is cache dir is absolute to system files
561
     * or relative to project destination?
562
     */
563
    public function isCacheDirIsAbsolute(): bool
564
    {
565
        $path = (string) $this->get('cache.dir');
1✔
566
        if (Util::joinFile($path) == realpath(Util::joinFile($path))) {
1✔
NEW
567
            return true;
×
568
        }
569

570
        return false;
1✔
571
    }
572

573
    /**
574
     * Set a Data object as configuration.
575
     */
576
    protected function setData(Data $data): self
577
    {
NEW
578
        if ($this->data !== $data) {
×
NEW
579
            $this->data = $data;
×
580
        }
581

NEW
582
        return $this;
×
583
    }
584

585
    /**
586
     * Get configuration as a Data object.
587
     */
588
    protected function getData(): Data
589
    {
590
        return $this->data;
1✔
591
    }
592

593
    /**
594
     * Valid the configuration.
595
     */
596
    private function valid(): void
597
    {
598
        // default language must be valid
599
        if (!preg_match('/^' . Config::LANG_CODE_PATTERN . '$/', (string) $this->get('language'))) {
1✔
NEW
600
            throw new ConfigException(sprintf('Default language code "%s" is not valid (e.g.: "language: fr-FR").', $this->get('language')));
×
601
        }
602
        // if language is set then the locale is required
603
        foreach ((array) $this->get('languages') as $lang) {
1✔
604
            if (!isset($lang['locale'])) {
1✔
NEW
605
                throw new ConfigException('A language locale is not defined.');
×
606
            }
607
            if (!preg_match('/^' . Config::LANG_LOCALE_PATTERN . '$/', $lang['locale'])) {
1✔
NEW
608
                throw new ConfigException(sprintf('The language locale "%s" is not valid (e.g.: "locale: fr_FR").', $lang['locale']));
×
609
            }
610
        }
611
        // Version 8.x breaking changes
612
        $toV8 = [
1✔
613
            'frontmatter'  => 'pages:frontmatter',
1✔
614
            'body'         => 'pages:body',
1✔
615
            'defaultpages' => 'pages:default',
1✔
616
            'virtualpages' => 'pages:virtual',
1✔
617
            'generators'   => 'pages:generators',
1✔
618
            'translations' => 'layouts:translations',
1✔
619
            'extensions'   => 'layouts:extensions',
1✔
620
            'postprocess'  => 'optimize',
1✔
621
        ];
1✔
622
        array_walk($toV8, function ($to, $from) {
1✔
623
            if ($this->has($from)) {
1✔
NEW
624
                $path = explode(':', $to);
×
NEW
625
                $step = 0;
×
NEW
626
                $formatedPath = '';
×
NEW
627
                foreach ($path as $fragment) {
×
NEW
628
                    $step = $step + 2;
×
NEW
629
                    $formatedPath .= "$fragment:\n" . str_pad(' ', $step);
×
630
                }
NEW
631
                throw new ConfigException("Option `$from:` must be moved to:\n```\n$formatedPath\n```");
×
632
            }
633
        });
1✔
634
    }
635
}
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

© 2025 Coveralls, Inc