Completed
Push — deprecated-options ( d50a2b...16bee4 )
by Arnaud
06:08 queued 06:00
created

Config   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 624
Duplicated Lines 0 %

Test Coverage

Coverage 78.39%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 167
dl 0
loc 624
ccs 156
cts 199
cp 0.7839
rs 2
c 3
b 1
f 0
wmc 93

42 Methods

Rating   Name   Duplication   Size   Complexity  
A getThemeDirPath() 0 3 1
A getTheme() 0 11 3
A getCacheAssetsPath() 0 3 1
A getLanguageIndex() 0 9 2
A setDestinationDir() 0 11 3
A getCachePath() 0 14 3
A has() 0 19 6
A getOutputFormatProperty() 0 9 2
A getThemesPath() 0 3 1
A getAssetsImagesWidths() 0 3 1
A getAssetsImagesSizes() 0 3 1
A getDestinationDir() 0 3 1
A getLanguageDefault() 0 14 4
B get() 0 19 7
A getStaticPath() 0 3 1
A getLayoutSection() 0 7 2
A castSetValue() 0 9 2
A getAssetsRemotePath() 0 3 1
A setSourceDir() 0 11 3
A getLayoutsInternalPath() 0 3 1
A hasTheme() 0 13 5
A getLanguageProperty() 0 11 2
A __construct() 0 12 1
A getSourceDir() 0 3 1
A getDataPath() 0 3 1
A getCacheTranslationsPath() 0 3 1
A getLayoutsPath() 0 3 1
A isCacheDirIsAbsolute() 0 8 2
A getCacheAssetsFilesPath() 0 3 1
A isEnabled() 0 10 5
A getLanguages() 0 17 4
A getStaticTargetPath() 0 9 2
A getCacheTemplatesPath() 0 3 1
A getAssetsPath() 0 3 1
A getOutputPath() 0 3 1
A getTranslationsPath() 0 3 1
A export() 0 3 1
A getTranslationsInternalPath() 0 7 2
B validate() 0 35 9
A import() 0 5 1
A getPagesPath() 0 3 1
A setFromEnv() 0 5 3

How to fix   Complexity   

Complex Class

Complex classes like Config often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Config, and based on these observations, apply Extract Interface, too.

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