Passed
Push — nested-sections ( 2d40b2...7dbf34 )
by Arnaud
12:03 queued 05:37
created

Config::valid()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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