Passed
Pull Request — master (#1676)
by Arnaud
14:31 queued 08:55
created

Config::getCacheAssetsPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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