Passed
Push — dependabot/composer/friendsofp... ( 1dbe24 )
by
unknown
05:59 queued 33s
created

Config::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 4
nop 1
dl 0
loc 18
rs 9.9666
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;
15
16
use Cecil\Exception\ConfigException;
17
use Cecil\Util\Platform;
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'));
53
        if (Platform::isPhar()) {
54
            $defaultConfigFile = Util::joinPath(Platform::getPharPath(), 'config/default.php');
55
        }
56
        $this->default = new Data(include $defaultConfigFile);
57
58
        // base configuration
59
        $baseConfigFile = realpath(Util::joinFile(__DIR__, '..', 'config/base.php'));
60
        if (Platform::isPhar()) {
61
            $baseConfigFile = Util::joinPath(Platform::getPharPath(), 'config/base.php');
62
        }
63
        $this->data = new Data(include $baseConfigFile);
64
65
        // import config
66
        $this->import($config ?? []);
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);
75
        $this->setFromEnv();
76
        $this->validate();
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);
97
98
        if ($language !== null) {
99
            $langIndex = $this->getLanguageIndex($language);
100
            $keyLang = "languages.$langIndex.config.$key";
101
            if ($this->data->has($keyLang)) {
102
                return true;
103
            }
104
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
105
                return $default;
106
            }
107
        }
108
        if ($this->data->has($key)) {
109
            return true;
110
        }
111
112
        return $default;
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;
127
128
        if ($language !== null) {
129
            $langIndex = $this->getLanguageIndex($language);
130
            $keyLang = "languages.$langIndex.config.$key";
131
            if ($this->data->has($keyLang)) {
132
                return $this->data->get($keyLang);
133
            }
134
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
135
                return $default;
136
            }
137
        }
138
        if ($this->data->has($key)) {
139
            return $this->data->get($key);
140
        }
141
142
        return $default;
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) {
153
            $sourceDir = getcwd();
154
        }
155
        if (!is_dir($sourceDir)) {
156
            throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid source.', $sourceDir));
157
        }
158
        $this->sourceDir = $sourceDir;
159
160
        return $this;
161
    }
162
163
    /**
164
     * Get the source directory.
165
     */
166
    public function getSourceDir(): string
167
    {
168
        return $this->sourceDir;
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) {
179
            $destinationDir = $this->sourceDir;
180
        }
181
        if (!is_dir($destinationDir)) {
182
            throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid destination.', $destinationDir));
183
        }
184
        $this->destinationDir = $destinationDir;
185
186
        return $this;
187
    }
188
189
    /**
190
     * Get the destination directory.
191
     */
192
    public function getDestinationDir(): string
193
    {
194
        return $this->destinationDir;
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'));
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'));
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'));
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'));
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'));
239
    }
240
241
    /**
242
     * Returns the layout for a section.
243
     */
244
    public function getLayoutSection(?string $section): ?string
245
    {
246
        if ($layout = $this->get('layouts.sections')[$section] ?? null) {
247
            return $layout;
248
        }
249
250
        return $section;
251
    }
252
253
    /**
254
     * Returns the path of translations directory.
255
     */
256
    public function getTranslationsPath(): string
257
    {
258
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.translations.dir'));
259
    }
260
261
    /**
262
     * Returns the path of internal translations directory.
263
     */
264
    public function getTranslationsInternalPath(): string
265
    {
266
        if (Util\Platform::isPhar()) {
267
            return Util::joinPath(Platform::getPharPath(), (string) $this->get('layouts.translations.internal.dir'));
268
        }
269
270
        return realpath(Util::joinPath(__DIR__, '..', (string) $this->get('layouts.translations.internal.dir')));
271
    }
272
273
    /**
274
     * Returns the path of themes directory.
275
     */
276
    public function getThemesPath(): string
277
    {
278
        return Util::joinFile($this->getSourceDir(), (string) $this->get('themes.dir'));
279
    }
280
281
    /**
282
     * Returns the path of static files directory.
283
     */
284
    public function getStaticPath(): string
285
    {
286
        return Util::joinFile($this->getSourceDir(), (string) $this->get('static.dir'));
287
    }
288
289
    /**
290
     * Returns the path of static files directory, with a target.
291
     */
292
    public function getStaticTargetPath(): string
293
    {
294
        $path = $this->getStaticPath();
295
296
        if (!empty($this->get('static.target'))) {
297
            $path = substr($path, 0, -\strlen((string) $this->get('static.target')));
298
        }
299
300
        return $path;
301
    }
302
303
    /**
304
     * Returns the path of assets files directory.
305
     */
306
    public function getAssetsPath(): string
307
    {
308
        return Util::joinFile($this->getSourceDir(), (string) $this->get('assets.dir'));
309
    }
310
311
    /**
312
     * Returns cache path.
313
     *
314
     * @throws ConfigException
315
     */
316
    public function getCachePath(): string
317
    {
318
        if (empty((string) $this->get('cache.dir'))) {
319
            throw new ConfigException(\sprintf('The cache directory ("%s") is not defined.', 'cache.dir'));
320
        }
321
322
        if ($this->isCacheDirIsAbsolute()) {
323
            $cacheDir = Util::joinFile((string) $this->get('cache.dir'), 'cecil');
324
            Util\File::getFS()->mkdir($cacheDir);
325
326
            return $cacheDir;
327
        }
328
329
        return Util::joinFile($this->getDestinationDir(), (string) $this->get('cache.dir'));
330
    }
331
332
    /**
333
     * Returns cache path of templates.
334
     */
335
    public function getCacheTemplatesPath(): string
336
    {
337
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.templates.dir'));
338
    }
339
340
    /**
341
     * Returns cache path of translations.
342
     */
343
    public function getCacheTranslationsPath(): string
344
    {
345
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.translations.dir'));
346
    }
347
348
    /**
349
     * Returns cache path of assets.
350
     */
351
    public function getCacheAssetsPath(): string
352
    {
353
        return Util::joinFile($this->getCachePath(), (string) $this->get('cache.assets.dir'));
354
    }
355
356
    /**
357
     * Returns cache path of remote assets.
358
     */
359
    public function getCacheAssetsRemotePath(): string
360
    {
361
        return Util::joinFile($this->getCacheAssetsPath(), (string) $this->get('cache.assets.remote.dir'));
362
    }
363
364
    /*
365
     * Output helpers.
366
     */
367
368
    /**
369
     * Returns the property value of an output format.
370
     *
371
     * @throws ConfigException
372
     */
373
    public function getOutputFormatProperty(string $name, string $property): string|array|null
374
    {
375
        $properties = array_column((array) $this->get('output.formats'), $property, 'name');
376
377
        if (empty($properties)) {
378
            throw new ConfigException(\sprintf('Property "%s" is not defined for format "%s".', $property, $name));
379
        }
380
381
        return $properties[$name] ?? null;
382
    }
383
384
    /*
385
     * Assets helpers.
386
     */
387
388
    /**
389
     * Returns asset image widths.
390
     */
391
    public function getAssetsImagesWidths(): array
392
    {
393
        return $this->get('assets.images.responsive.widths');
394
    }
395
396
    /**
397
     * Returns asset image sizes.
398
     */
399
    public function getAssetsImagesSizes(): array
400
    {
401
        return $this->get('assets.images.responsive.sizes');
402
    }
403
404
    /*
405
     * Theme helpers.
406
     */
407
408
    /**
409
     * Returns theme(s) as an array.
410
     */
411
    public function getTheme(): ?array
412
    {
413
        if ($themes = $this->get('theme')) {
414
            if (\is_array($themes)) {
415
                return $themes;
416
            }
417
418
            return [$themes];
419
        }
420
421
        return null;
422
    }
423
424
    /**
425
     * Has a (valid) theme(s)?
426
     *
427
     * @throws ConfigException
428
     */
429
    public function hasTheme(): bool
430
    {
431
        if ($themes = $this->getTheme()) {
432
            foreach ($themes as $theme) {
433
                if (!Util\File::getFS()->exists($this->getThemeDirPath($theme, 'layouts')) && !Util\File::getFS()->exists(Util::joinFile($this->getThemesPath(), $theme, 'config.yml'))) {
434
                    throw new ConfigException(\sprintf('Theme "%s" not found. Did you forgot to install it?', $theme));
435
                }
436
            }
437
438
            return true;
439
        }
440
441
        return false;
442
    }
443
444
    /**
445
     * Returns the path of a specific theme's directory.
446
     * ("layouts" by default).
447
     */
448
    public function getThemeDirPath(string $theme, string $dir = 'layouts'): string
449
    {
450
        return Util::joinFile($this->getThemesPath(), $theme, $dir);
451
    }
452
453
    /*
454
     * Language helpers.
455
     */
456
457
    /**
458
     * Returns an array of available languages.
459
     *
460
     * @throws ConfigException
461
     */
462
    public function getLanguages(): array
463
    {
464
        if ($this->languages !== null) {
465
            return $this->languages;
466
        }
467
468
        $languages = array_filter((array) $this->get('languages'), function ($language) {
469
            return !(isset($language['enabled']) && $language['enabled'] === false);
470
        });
471
472
        if (!\is_int(array_search($this->getLanguageDefault(), array_column($languages, 'code')))) {
473
            throw new ConfigException(\sprintf('The default language "%s" is not listed in "languages".', $this->getLanguageDefault()));
474
        }
475
476
        $this->languages = $languages;
477
478
        return $this->languages;
479
    }
480
481
    /**
482
     * Returns the default language code (ie: "en", "fr-FR", etc.).
483
     *
484
     * @throws ConfigException
485
     */
486
    public function getLanguageDefault(): string
487
    {
488
        if (!$this->get('language')) {
489
            throw new ConfigException('There is no default "language" key.');
490
        }
491
        if (\is_array($this->get('language'))) {
492
            if (!$this->get('language.code')) {
493
                throw new ConfigException('There is no "language.code" key.');
494
            }
495
496
            return $this->get('language.code');
497
        }
498
499
        return $this->get('language');
500
    }
501
502
    /**
503
     * Returns a language code index.
504
     *
505
     * @throws ConfigException
506
     */
507
    public function getLanguageIndex(string $code): int
508
    {
509
        $array = array_column($this->getLanguages(), 'code');
510
511
        if (false === $index = array_search($code, $array)) {
512
            throw new ConfigException(\sprintf('The language code "%s" is not defined.', $code));
513
        }
514
515
        return $index;
516
    }
517
518
    /**
519
     * Returns the property value of a (specified or the default) language.
520
     *
521
     * @throws ConfigException
522
     */
523
    public function getLanguageProperty(string $property, ?string $code = null): string
524
    {
525
        $code = $code ?? $this->getLanguageDefault();
526
527
        $properties = array_column($this->getLanguages(), $property, 'code');
528
529
        if (empty($properties)) {
530
            throw new ConfigException(\sprintf('Property "%s" is not defined for language "%s".', $property, $code));
531
        }
532
533
        return $properties[$code];
534
    }
535
536
    /*
537
     * Cache helpers.
538
     */
539
540
    /**
541
     * Is cache dir is absolute to system files
542
     * or relative to project destination?
543
     */
544
    public function isCacheDirIsAbsolute(): bool
545
    {
546
        $path = (string) $this->get('cache.dir');
547
        if (Util::joinFile($path) == realpath(Util::joinFile($path))) {
548
            return true;
549
        }
550
551
        return false;
552
    }
553
554
    /**
555
     * Set configuration from environment variables.
556
     */
557
    private function setFromEnv(): void
558
    {
559
        foreach (getenv() as $key => $value) {
560
            if (str_starts_with($key, 'CECIL_')) {
561
                $this->data->set(str_replace(['cecil_', '_'], ['', '.'], strtolower($key)), $this->castSetValue($value));
562
            }
563
        }
564
    }
565
566
    /**
567
     * Casts boolean value given to set() as string.
568
     *
569
     * @param mixed $value
570
     *
571
     * @return bool|mixed
572
     */
573
    private function castSetValue($value)
574
    {
575
        $filteredValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
576
577
        if ($filteredValue !== null) {
578
            return $filteredValue;
579
        }
580
581
        return $value;
582
    }
583
584
    /**
585
     * Validate the configuration.
586
     *
587
     * @throws ConfigException
588
     */
589
    private function validate(): void
590
    {
591
        // default language must be valid
592
        if (!preg_match('/^' . Config::LANG_CODE_PATTERN . '$/', $this->getLanguageDefault())) {
593
            throw new ConfigException(\sprintf('Default language code "%s" is not valid (e.g.: "language: fr-FR").', $this->getLanguageDefault()));
594
        }
595
        // if language is set then the locale is required and must be valid
596
        foreach ((array) $this->get('languages') as $lang) {
597
            if (!isset($lang['locale'])) {
598
                throw new ConfigException('A language locale is not defined.');
599
            }
600
            if (!preg_match('/^' . Config::LANG_LOCALE_PATTERN . '$/', $lang['locale'])) {
601
                throw new ConfigException(\sprintf('The language locale "%s" is not valid (e.g.: "locale: fr_FR").', $lang['locale']));
602
            }
603
        }
604
605
        // version 8.x breaking changes detection
606
        $toV8 = [
607
            'frontmatter'  => 'pages:frontmatter',
608
            'body'         => 'pages:body',
609
            'defaultpages' => 'pages:default',
610
            'virtualpages' => 'pages:virtual',
611
            'generators'   => 'pages:generators',
612
            'translations' => 'layouts:translations',
613
            'extensions'   => 'layouts:extensions',
614
            'postprocess'  => 'optimize',
615
        ];
616
        array_walk($toV8, function ($to, $from) {
617
            if ($this->has($from)) {
618
                $path = explode(':', $to);
619
                $step = 0;
620
                $formatedPath = '';
621
                foreach ($path as $fragment) {
622
                    $step = $step + 2;
623
                    $formatedPath .= "$fragment:\n" . str_pad(' ', $step);
624
                }
625
                throw new ConfigException("Option `$from:` must be moved to:\n```\n$formatedPath\n```");
626
            }
627
        });
628
    }
629
}
630