Test Failed
Pull Request — master (#2068)
by Arnaud
08:43 queued 03:59
created

Config::getLanguageDefault()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 5.024

Importance

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