Passed
Pull Request — master (#2148)
by Arnaud
09:07 queued 03:58
created

Config::isEnabled()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5.1158

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 5
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
crap 5.1158
rs 9.6111
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 Dflydev\DotAccessData\Data;
18
19
/**
20
 * Class Config.
21
 */
22
class Config
23
{
24
    /** Configuration is a Data object. */
25
    protected Data $data;
26
27
    /** Default configuration is a Data object. */
28
    protected Data $default;
29
30
    /** Source directory. */
31
    protected string $sourceDir;
32
33
    /** Destination directory. */
34
    protected string $destinationDir;
35
36
    /** Languages list as array. */
37
    protected ?array $languages = null;
38
39
    public const PRESERVE = 0;
40
    public const REPLACE = 1;
41
    public const MERGE = 2;
42
    public const LANG_CODE_PATTERN = '([a-z]{2}(-[A-Z]{2})?)'; // "fr" or "fr-FR"
43
    public const LANG_LOCALE_PATTERN = '[a-z]{2}(_[A-Z]{2})?(_[A-Z]{2})?'; // "fr" or "fr_FR" or "no_NO_NY"
44
45
    /**
46
     * Build the Config object with the default config + the optional given array.
47
     */
48 1
    public function __construct(?array $config = null)
49
    {
50
        // default configuration
51 1
        $defaultConfigFile = Util\File::getRealPath('../config/default.php');
52 1
        $this->default = new Data(include $defaultConfigFile);
53
        // base configuration
54 1
        $baseConfigFile = Util\File::getRealPath('../config/base.php');
55 1
        $this->data = new Data(include $baseConfigFile);
56
        // import config array if provided
57 1
        if ($config !== null) {
58 1
            $this->import($config);
59
        }
60
    }
61
62
    /**
63
     * Imports (and validate) configuration.
64
     * The mode can be: Config::PRESERVE, Config::REPLACE or Config::MERGE.
65
     */
66 1
    public function import(array $config, $mode = self::MERGE): void
67
    {
68 1
        $this->data->import($config, $mode);
69 1
        $this->setFromEnv(); // override configuration with environment variables
70 1
        $this->validate();   // validate configuration
71
    }
72
73
    /**
74
     * Get configuration as an array.
75
     */
76
    public function export(): array
77
    {
78
        return $this->data->export();
79
    }
80
81
    /**
82
     * Is configuration's key exists?
83
     *
84
     * @param string $key      Configuration key
85
     * @param string $language Language code (optional)
86
     * @param bool   $fallback Set to false to not return the value in the default language as fallback
87
     */
88 1
    public function has(string $key, ?string $language = null, bool $fallback = true): bool
89
    {
90 1
        $default = $this->default->has($key);
91
92 1
        if ($language !== null) {
93
            $langIndex = $this->getLanguageIndex($language);
94
            $keyLang = "languages.$langIndex.config.$key";
95
            if ($this->data->has($keyLang)) {
96
                return true;
97
            }
98
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
99
                return $default;
100
            }
101
        }
102 1
        if ($this->data->has($key)) {
103 1
            return true;
104
        }
105
106 1
        return $default;
107
    }
108
109
    /**
110
     * Get the value of a configuration's key.
111
     *
112
     * @param string $key      Configuration key
113
     * @param string $language Language code (optional)
114
     * @param bool   $fallback Set to false to not return the value in the default language as fallback
115
     *
116
     * @return mixed|null
117
     */
118 1
    public function get(string $key, ?string $language = null, bool $fallback = true)
119
    {
120 1
        $default = $this->default->has($key) ? $this->default->get($key) : null;
121 1
        if ($language !== null) {
122 1
            $langIndex = $this->getLanguageIndex($language);
123 1
            $keyLang = "languages.$langIndex.config.$key";
124 1
            if ($this->data->has($keyLang)) {
125 1
                return $this->data->get($keyLang);
126
            }
127 1
            if ($language !== $this->getLanguageDefault() && $fallback === false) {
128 1
                return $default;
129
            }
130
        }
131 1
        if ($this->data->has($key)) {
132 1
            return $this->data->get($key);
133
        }
134
135 1
        return $default;
136
    }
137
138
    /**
139
     * Is an option is enabled?
140
     * Checks if the key is set to `false` or if subkey `enabled` is set to `false`.
141
     */
142 1
    public function isEnabled(string $key, ?string $language = null, bool $fallback = true): bool
143
    {
144 1
        if ($this->has($key, $language, $fallback) && $this->get($key, $language, $fallback) !== false) {
145 1
            return true;
146
        }
147 1
        if ($this->has("$key.enabled", $language, $fallback) && $this->get("$key.enabled", $language, $fallback) === false) {
148
            return false;
149
        }
150
151 1
        return false;
152
    }
153
154
    /**
155
     * Set the source directory.
156
     *
157
     * @throws \InvalidArgumentException
158
     */
159 1
    public function setSourceDir(string $sourceDir): self
160
    {
161 1
        if (!is_dir($sourceDir)) {
162
            throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid source.', $sourceDir));
163
        }
164 1
        $this->sourceDir = $sourceDir;
165
166 1
        return $this;
167
    }
168
169
    /**
170
     * Get the source directory.
171
     */
172 1
    public function getSourceDir(): string
173
    {
174 1
        if ($this->sourceDir === null) {
175
            return getcwd();
176
        }
177
178 1
        return $this->sourceDir;
179
    }
180
181
    /**
182
     * Set the destination directory.
183
     *
184
     * @throws \InvalidArgumentException
185
     */
186 1
    public function setDestinationDir(string $destinationDir): self
187
    {
188 1
        if (!is_dir($destinationDir)) {
189
            throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid destination.', $destinationDir));
190
        }
191 1
        $this->destinationDir = $destinationDir;
192
193 1
        return $this;
194
    }
195
196
    /**
197
     * Get the destination directory.
198
     */
199 1
    public function getDestinationDir(): string
200
    {
201 1
        if ($this->destinationDir === null) {
202
            return $this->getSourceDir();
203
        }
204
205 1
        return $this->destinationDir;
206
    }
207
208
    /*
209
     * Path helpers.
210
     */
211
212
    /**
213
     * Returns the path of the pages directory.
214
     */
215 1
    public function getPagesPath(): string
216
    {
217 1
        return Util::joinFile($this->getSourceDir(), (string) $this->get('pages.dir'));
218
    }
219
220
    /**
221
     * Returns the path of the output directory.
222
     */
223 1
    public function getOutputPath(): string
224
    {
225 1
        return Util::joinFile($this->getDestinationDir(), (string) $this->get('output.dir'));
226
    }
227
228
    /**
229
     * Returns the path of the data directory.
230
     */
231 1
    public function getDataPath(): string
232
    {
233 1
        return Util::joinFile($this->getSourceDir(), (string) $this->get('data.dir'));
234
    }
235
236
    /**
237
     * Returns the path of templates directory.
238
     */
239 1
    public function getLayoutsPath(): string
240
    {
241 1
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.dir'));
242
    }
243
244
    /**
245
     * Returns the path of internal templates directory.
246
     */
247 1
    public function getLayoutsInternalPath(): string
248
    {
249 1
        return __DIR__ . '/../resources/layouts';
250
    }
251
252
    /**
253
     * Returns the layout for a section.
254
     */
255 1
    public function getLayoutSection(?string $section): ?string
256
    {
257 1
        if ($layout = $this->get('layouts.sections')[$section] ?? null) {
258
            return $layout;
259
        }
260
261 1
        return $section;
262
    }
263
264
    /**
265
     * Returns the path of translations directory.
266
     */
267 1
    public function getTranslationsPath(): string
268
    {
269 1
        return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.translations.dir'));
270
    }
271
272
    /**
273
     * Returns the path of internal translations directory.
274
     */
275 1
    public function getTranslationsInternalPath(): string
276
    {
277 1
        return Util\File::getRealPath('../resources/translations');
278
    }
279
280
    /**
281
     * Returns the path of themes directory.
282
     */
283 1
    public function getThemesPath(): string
284
    {
285 1
        return Util::joinFile($this->getSourceDir(), 'themes');
286
    }
287
288
    /**
289
     * Returns the path of static files directory.
290
     */
291 1
    public function getStaticPath(): string
292
    {
293 1
        return Util::joinFile($this->getSourceDir(), (string) $this->get('static.dir'));
294
    }
295
296
    /**
297
     * Returns the path of static files directory, with a target.
298
     */
299 1
    public function getStaticTargetPath(): string
300
    {
301 1
        $path = $this->getStaticPath();
302 1
        if (!empty($this->get('static.target'))) {
303
            $path = substr($path, 0, -\strlen((string) $this->get('static.target')));
304
        }
305
306 1
        return $path;
307
    }
308
309
    /**
310
     * Returns the path of assets files directory.
311
     */
312 1
    public function getAssetsPath(): string
313
    {
314 1
        return Util::joinFile($this->getSourceDir(), (string) $this->get('assets.dir'));
315
    }
316
317
    /**
318
     * Returns the path of remote assets files directory (in cache).
319
     *
320
     * @return string
321
     */
322 1
    public function getAssetsRemotePath(): string
323
    {
324 1
        return Util::joinFile($this->getCacheAssetsPath(), 'remote');
325
    }
326
327
    /**
328
     * Returns cache path.
329
     *
330
     * @throws ConfigException
331
     */
332 1
    public function getCachePath(): string
333
    {
334 1
        if (empty((string) $this->get('cache.dir'))) {
335
            throw new ConfigException(\sprintf('The cache directory (`%s`) is not defined.', 'cache.dir'));
336
        }
337 1
        if ($this->isCacheDirIsAbsolute()) {
338
            $cacheDir = Util::joinFile((string) $this->get('cache.dir'), 'cecil');
339
            Util\File::getFS()->mkdir($cacheDir);
340
341
            return $cacheDir;
342
        }
343
344 1
        return Util::joinFile($this->getDestinationDir(), (string) $this->get('cache.dir'));
345
    }
346
347
    /**
348
     * Returns cache path of templates.
349
     */
350 1
    public function getCacheTemplatesPath(): string
351
    {
352 1
        return Util::joinFile($this->getCachePath(), 'templates');
353
    }
354
355
    /**
356
     * Returns cache path of translations.
357
     */
358 1
    public function getCacheTranslationsPath(): string
359
    {
360 1
        return Util::joinFile($this->getCachePath(), 'translations');
361
    }
362
363
    /**
364
     * Returns cache path of assets.
365
     */
366 1
    public function getCacheAssetsPath(): string
367
    {
368 1
        return Util::joinFile($this->getCachePath(), 'assets');
369
    }
370
371
    /**
372
     * Returns cache path of assets files.
373
     */
374 1
    public function getCacheAssetsFilesPath(): string
375
    {
376 1
        return Util::joinFile($this->getCacheAssetsPath(), 'files');
377
    }
378
379
    /*
380
     * Output helpers.
381
     */
382
383
    /**
384
     * Returns the property value of an output format.
385
     *
386
     * @throws ConfigException
387
     */
388 1
    public function getOutputFormatProperty(string $name, string $property): string|array|null
389
    {
390 1
        $properties = array_column((array) $this->get('output.formats'), $property, 'name');
391 1
        if (empty($properties)) {
392
            throw new ConfigException(\sprintf('Property "%s" is not defined for format "%s".', $property, $name));
393
        }
394
395 1
        return $properties[$name] ?? null;
396
    }
397
398
    /*
399
     * Assets helpers.
400
     */
401
402
    /**
403
     * Returns asset image widths.
404
     * Default: [480, 640, 768, 1024, 1366, 1600, 1920].
405
     */
406 1
    public function getAssetsImagesWidths(): array
407
    {
408 1
        return $this->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920];
409
    }
410
411
    /**
412
     * Returns asset image sizes.
413
     * Default: ['default' => '100vw'].
414
     */
415 1
    public function getAssetsImagesSizes(): array
416
    {
417 1
        return $this->get('assets.images.responsive.sizes') ?? ['default' => '100vw'];
418
    }
419
420
    /*
421
     * Theme helpers.
422
     */
423
424
    /**
425
     * Returns theme(s) as an array.
426
     */
427 1
    public function getTheme(): ?array
428
    {
429 1
        if ($themes = $this->get('theme')) {
430 1
            if (\is_array($themes)) {
431 1
                return $themes;
432
            }
433
434
            return [$themes];
435
        }
436
437
        return null;
438
    }
439
440
    /**
441
     * Has a (valid) theme(s)?
442
     *
443
     * @throws ConfigException
444
     */
445 1
    public function hasTheme(): bool
446
    {
447 1
        if ($themes = $this->getTheme()) {
448 1
            foreach ($themes as $theme) {
449 1
                if (!Util\File::getFS()->exists($this->getThemeDirPath($theme, 'layouts')) && !Util\File::getFS()->exists(Util::joinFile($this->getThemesPath(), $theme, 'config.yml'))) {
450
                    throw new ConfigException(\sprintf('Theme "%s" not found. Did you forgot to install it?', $theme));
451
                }
452
            }
453
454 1
            return true;
455
        }
456
457
        return false;
458
    }
459
460
    /**
461
     * Returns the path of a specific theme's directory.
462
     * ("layouts" by default).
463
     */
464 1
    public function getThemeDirPath(string $theme, string $dir = 'layouts'): string
465
    {
466 1
        return Util::joinFile($this->getThemesPath(), $theme, $dir);
467
    }
468
469
    /*
470
     * Language helpers.
471
     */
472
473
    /**
474
     * Returns an array of available languages.
475
     *
476
     * @throws ConfigException
477
     */
478 1
    public function getLanguages(): array
479
    {
480 1
        if ($this->languages !== null) {
481 1
            return $this->languages;
482
        }
483 1
        $languages = array_filter((array) $this->get('languages'), function ($language) {
484 1
            return !(isset($language['enabled']) && $language['enabled'] === false);
485 1
        });
486 1
        if (!\is_int(array_search($this->getLanguageDefault(), array_column($languages, 'code')))) {
487
            throw new ConfigException(\sprintf('The default language "%s" is not listed in "languages".', $this->getLanguageDefault()));
488
        }
489 1
        $this->languages = $languages;
490
491 1
        return $this->languages;
492
    }
493
494
    /**
495
     * Returns the default language code (ie: "en", "fr-FR", etc.).
496
     *
497
     * @throws ConfigException
498
     */
499 1
    public function getLanguageDefault(): string
500
    {
501 1
        if (!$this->get('language')) {
502
            throw new ConfigException('There is no default "language" key.');
503
        }
504 1
        if (\is_array($this->get('language'))) {
505
            if (!$this->get('language.code')) {
506
                throw new ConfigException('There is no "language.code" key.');
507
            }
508
509
            return $this->get('language.code');
510
        }
511
512 1
        return $this->get('language');
513
    }
514
515
    /**
516
     * Returns a language code index.
517
     *
518
     * @throws ConfigException
519
     */
520 1
    public function getLanguageIndex(string $code): int
521
    {
522 1
        $array = array_column($this->getLanguages(), 'code');
523 1
        if (false === $index = array_search($code, $array)) {
524
            throw new ConfigException(\sprintf('The language code "%s" is not defined.', $code));
525
        }
526
527 1
        return $index;
528
    }
529
530
    /**
531
     * Returns the property value of a (specified or the default) language.
532
     *
533
     * @throws ConfigException
534
     */
535 1
    public function getLanguageProperty(string $property, ?string $code = null): string
536
    {
537 1
        $code = $code ?? $this->getLanguageDefault();
538 1
        $properties = array_column($this->getLanguages(), $property, 'code');
539 1
        if (empty($properties)) {
540
            throw new ConfigException(\sprintf('Property "%s" is not defined for language "%s".', $property, $code));
541
        }
542
543 1
        return $properties[$code];
544
    }
545
546
    /*
547
     * Cache helpers.
548
     */
549
550
    /**
551
     * Is cache dir is absolute to system files
552
     * or relative to project destination?
553
     */
554 1
    public function isCacheDirIsAbsolute(): bool
555
    {
556 1
        $path = (string) $this->get('cache.dir');
557 1
        if (Util::joinFile($path) == realpath(Util::joinFile($path))) {
558
            return true;
559
        }
560
561 1
        return false;
562
    }
563
564
    /*
565
     * Private functions.
566
     */
567
568
    /**
569
     * Set configuration from environment variables starting with "CECIL_".
570
     */
571 1
    private function setFromEnv(): void
572
    {
573 1
        foreach (getenv() as $key => $value) {
574 1
            if (str_starts_with($key, 'CECIL_')) {
575 1
                $this->data->set(str_replace(['cecil_', '_'], ['', '.'], strtolower($key)), $this->castSetValue($value));
576
            }
577
        }
578
    }
579
580
    /**
581
     * Casts boolean value given to set() as string.
582
     *
583
     * @param mixed $value
584
     *
585
     * @return bool|mixed
586
     */
587 1
    private function castSetValue($value)
588
    {
589 1
        $filteredValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
590
591 1
        if ($filteredValue !== null) {
592 1
            return $filteredValue;
593
        }
594
595 1
        return $value;
596
    }
597
598
    /**
599
     * Validate the configuration.
600
     *
601
     * @throws ConfigException
602
     */
603 1
    private function validate(): void
604
    {
605
        // default language must be valid
606 1
        if (!preg_match('/^' . Config::LANG_CODE_PATTERN . '$/', $this->getLanguageDefault())) {
607
            throw new ConfigException(\sprintf('Default language code "%s" is not valid (e.g.: "language: fr-FR").', $this->getLanguageDefault()));
608
        }
609
        // if language is set then the locale is required and must be valid
610 1
        foreach ((array) $this->get('languages') as $lang) {
611 1
            if (!isset($lang['locale'])) {
612
                throw new ConfigException('A language locale is not defined.');
613
            }
614 1
            if (!preg_match('/^' . Config::LANG_LOCALE_PATTERN . '$/', $lang['locale'])) {
615
                throw new ConfigException(\sprintf('The language locale "%s" is not valid (e.g.: "locale: fr_FR").', $lang['locale']));
616
            }
617
        }
618
619
        // check for deprecated options
620 1
        $deprecatedConfigFile = Util\File::getRealPath('../config/deprecated.php');
621 1
        $deprecatedConfig = require $deprecatedConfigFile;
622 1
        array_walk($deprecatedConfig, function ($to, $from) {
623 1
            if ($this->has($from)) {
624
                if (empty($to)) {
625
                    throw new ConfigException("Option `$from` is deprecated and must be removed.");
626
                }
627
                $path = explode(':', $to);
628
                $step = 0;
629
                $formatedPath = '';
630
                foreach ($path as $fragment) {
631
                    $step = $step + 2;
632
                    $formatedPath .= "$fragment:\n" . str_pad(' ', $step);
633
                }
634
                throw new ConfigException("Option `$from` must be moved to:\n```\n$formatedPath\n```");
635
            }
636 1
        });
637
    }
638
}
639