Test Failed
Pull Request — master (#1676)
by Arnaud
08:22 queued 03:40
created

Config::valid()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 7

Importance

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