Passed
Push — master ( 9c2913...44febc )
by Arnaud
05:59
created

Builder::getVersion()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.024

Importance

Changes 0
Metric Value
cc 4
eloc 10
c 0
b 0
f 0
nc 6
nop 0
dl 0
loc 16
rs 9.9332
ccs 6
cts 10
cp 0.6
crap 5.024
1
<?php
2
3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cecil;
15
16
use Cecil\Collection\Page\Collection as PagesCollection;
17
use Cecil\Exception\RuntimeException;
18
use Cecil\Generator\GeneratorManager;
19
use Cecil\Logger\PrintLogger;
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerInterface;
22
use Symfony\Component\Finder\Finder;
23
24
/**
25
 * The main Cecil builder class.
26
 *
27
 * This class is responsible for building the website by processing various steps,
28
 * managing configuration, and handling content, data, static files, pages, assets,
29
 * menus, taxonomies, and rendering.
30
 * It also provides methods for logging, debugging, and managing build metrics.
31
 */
32
class Builder implements LoggerAwareInterface
33
{
34
    public const VERSION = '8.x-dev';
35
    public const VERBOSITY_QUIET = -1;
36
    public const VERBOSITY_NORMAL = 0;
37
    public const VERBOSITY_VERBOSE = 1;
38
    public const VERBOSITY_DEBUG = 2;
39
    /**
40
     * Default options for the build process.
41
     * These options can be overridden when calling the build() method.
42
     * - 'drafts': if true, builds drafts too (default: false)
43
     * - 'dry-run': if true, generated files are not saved (default: false)
44
     * - 'page': if specified, only this page is processed (default: '')
45
     * - 'render-subset': limits the render step to a specific subset (default: '')
46
     * @var array<string, bool|string>
47
     * @see \Cecil\Builder::build()
48
     */
49
    public const OPTIONS = [
50
        'drafts'  => false,
51
        'dry-run' => false,
52
        'page'    => '',
53
        'render-subset' => '',
54
    ];
55
56
    /**
57
     * Steps processed by build(), in order.
58
     * These steps are executed sequentially to build the website.
59
     * Each step is a class that implements the StepInterface.
60
     * @var array<string>
61
     * @see \Cecil\Step\StepInterface
62
     */
63
    protected $steps = [
64
        'Cecil\Step\Pages\Load',
65
        'Cecil\Step\Data\Load',
66
        'Cecil\Step\StaticFiles\Load',
67
        'Cecil\Step\Pages\Create',
68
        'Cecil\Step\Pages\Convert',
69
        'Cecil\Step\Taxonomies\Create',
70
        'Cecil\Step\Pages\Generate',
71
        'Cecil\Step\Menus\Create',
72
        'Cecil\Step\StaticFiles\Copy',
73
        'Cecil\Step\Pages\Render',
74
        'Cecil\Step\Pages\Save',
75
        'Cecil\Step\Assets\Save',
76
        'Cecil\Step\Optimize\Html',
77
        'Cecil\Step\Optimize\Css',
78
        'Cecil\Step\Optimize\Js',
79
        'Cecil\Step\Optimize\Images',
80
    ];
81
    /**
82
     * Configuration object.
83
     * This object holds all the configuration settings for the build process.
84
     * It can be set to an array or a Config instance.
85
     * @var Config|array|null
86
     * @see \Cecil\Config
87
     */
88
    protected $config;
89
    /**
90
     * Logger instance.
91
     * This logger is used to log messages during the build process.
92
     * It can be set to any PSR-3 compliant logger.
93
     * @var LoggerInterface
94
     * @see \Psr\Log\LoggerInterface
95
     * */
96
    protected $logger;
97
    /**
98
     * Debug mode state.
99
     * If true, debug messages are logged.
100
     * @var bool
101
     */
102
    protected $debug = false;
103
    /**
104
     * Build options.
105
     * These options can be passed to the build() method to customize the build process.
106
     * @var array
107
     * @see \Cecil\Builder::OPTIONS
108
     * @see \Cecil\Builder::build()
109
     */
110
    protected $options = [];
111
    /**
112
     * Content files collection.
113
     * This is a Finder instance that collects all the content files (pages, posts, etc.) from the source directory.
114
     * @var Finder
115
     */
116
    protected $content;
117
    /**
118
     * Data collection.
119
     * This is an associative array that holds data loaded from YAML files in the data directory.
120
     * @var array
121
     */
122
    protected $data = [];
123
    /**
124
     * Static files collection.
125
     * This is an associative array that holds static files (like images, CSS, JS) that are copied to the destination directory.
126
     * @var array
127
     */
128
    protected $static = [];
129
    /**
130
     * Pages collection.
131
     * This is a collection of pages that have been processed and are ready for rendering.
132
     * It is an instance of PagesCollection, which is a custom collection class for managing pages.
133
     * @var PagesCollection
134
     */
135
    protected $pages;
136
    /**
137
     * Assets path collection.
138
     * This is an array that holds paths to assets (like CSS, JS, images) that are used in the build process.
139
     * It is used to keep track of assets that need to be processed or copied.
140
     * It can be set to an array of paths or updated with new asset paths.
141
     * @var array
142
     */
143
    protected $assets = [];
144
    /**
145
     * Menus collection.
146
     * This is an associative array that holds menus for different languages.
147
     * Each key is a language code, and the value is a Collection\Menu\Collection instance
148
     * that contains the menu items for that language.
149
     * It is used to manage navigation menus across different languages in the website.
150
     * @var array
151
     * @see \Cecil\Collection\Menu\Collection
152
     */
153
    protected $menus;
154
    /**
155
     * Taxonomies collection.
156
     * This is an associative array that holds taxonomies for different languages.
157
     * Each key is a language code, and the value is a Collection\Taxonomy\Collection instance
158
     * that contains the taxonomy terms for that language.
159
     * It is used to manage taxonomies (like categories, tags) across different languages in the website.
160
     * @var array
161
     * @see \Cecil\Collection\Taxonomy\Collection
162
     */
163
    protected $taxonomies;
164
    /**
165
     * Renderer.
166
     * This is an instance of Renderer\RendererInterface that is responsible for rendering pages.
167
     * It handles the rendering of templates and the application of data to those templates.
168
     * @var Renderer\RendererInterface
169
     */
170
    protected $renderer;
171
    /**
172
     * Generators manager.
173
     * This is an instance of GeneratorManager that manages all the generators used in the build process.
174
     * Generators are used to create dynamic content or perform specific tasks during the build.
175
     * It allows for the registration and execution of various generators that can extend the functionality of the build process.
176
     * @var GeneratorManager
177
     */
178
    protected $generatorManager;
179
    /**
180
     * Application version.
181
     * @var string
182
     */
183
    protected static $version;
184
    /**
185
     * Build metrics.
186
     * This array holds metrics about the build process, such as duration and memory usage for each step.
187
     * It is used to track the performance of the build and can be useful for debugging and optimization.
188
     * @var array
189
     */
190
    protected $metrics = [];
191
    /**
192
     * Current build ID.
193
     * This is a unique identifier for the current build process.
194
     * It is generated based on the current date and time when the build starts.
195
     * It can be used to track builds, especially in environments where multiple builds may occur.
196
     * @var string
197
     * @see \Cecil\Builder::build()
198
     */
199
    protected $buildId;
200
201
    /**
202
     * @param Config|array|null    $config
203
     * @param LoggerInterface|null $logger
204
     */
205 1
    public function __construct($config = null, ?LoggerInterface $logger = null)
206
    {
207
        // init and set config
208 1
        $this->config = new Config();
209 1
        if ($config !== null) {
210 1
            $this->setConfig($config);
211
        }
212
        // debug mode?
213 1
        if (getenv('CECIL_DEBUG') == 'true' || $this->getConfig()->isEnabled('debug')) {
214 1
            $this->debug = true;
215
        }
216
        // set logger
217 1
        if ($logger === null) {
218
            $logger = new PrintLogger(self::VERBOSITY_VERBOSE);
219
        }
220 1
        $this->setLogger($logger);
221
    }
222
223
    /**
224
     * Creates a new Builder instance.
225
     */
226 1
    public static function create(): self
227
    {
228 1
        $class = new \ReflectionClass(\get_called_class());
229
230 1
        return $class->newInstanceArgs(\func_get_args());
231
    }
232
233
    /**
234
     * Builds a new website.
235
     * This method processes the build steps in order, collects content, data, static files,
236
     * generates pages, renders them, and saves the output to the destination directory.
237
     * It also collects metrics about the build process, such as duration and memory usage.
238
     * @param array<string, self::OPTIONS> $options
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, self::OPTIONS> at position 4 could not be parsed: Expected '>' at position 4, but found 'self'.
Loading history...
239
     * @see \Cecil\Builder::OPTIONS
240
     */
241 1
    public function build(array $options): self
242
    {
243
        // set start script time and memory usage
244 1
        $startTime = microtime(true);
245 1
        $startMemory = memory_get_usage();
246
247
        // checks soft errors
248 1
        $this->checkErrors();
249
250
        // merge options with defaults
251 1
        $this->options = array_merge(self::OPTIONS, $options);
252
253
        // set build ID
254 1
        $this->buildId = date('YmdHis');
255
256
        // process each step
257 1
        $steps = [];
258
        // init...
259 1
        foreach ($this->steps as $step) {
260
            /** @var Step\StepInterface $stepObject */
261 1
            $stepObject = new $step($this);
262 1
            $stepObject->init($this->options);
263 1
            if ($stepObject->canProcess()) {
264 1
                $steps[] = $stepObject;
265
            }
266
        }
267
        // ...and process
268 1
        $stepNumber = 0;
269 1
        $stepsTotal = \count($steps);
270 1
        foreach ($steps as $step) {
271 1
            $stepNumber++;
272
            /** @var Step\StepInterface $step */
273 1
            $this->getLogger()->notice($step->getName(), ['step' => [$stepNumber, $stepsTotal]]);
274 1
            $stepStartTime = microtime(true);
275 1
            $stepStartMemory = memory_get_usage();
276 1
            $step->process();
277
            // step duration and memory usage
278 1
            $this->metrics['steps'][$stepNumber]['name'] = $step->getName();
279 1
            $this->metrics['steps'][$stepNumber]['duration'] = Util::convertMicrotime((float) $stepStartTime);
280 1
            $this->metrics['steps'][$stepNumber]['memory']   = Util::convertMemory(memory_get_usage() - $stepStartMemory);
281 1
            $this->getLogger()->info(\sprintf(
282 1
                '%s done in %s (%s)',
283 1
                $this->metrics['steps'][$stepNumber]['name'],
284 1
                $this->metrics['steps'][$stepNumber]['duration'],
285 1
                $this->metrics['steps'][$stepNumber]['memory']
286 1
            ));
287
        }
288
        // build duration and memory usage
289 1
        $this->metrics['total']['duration'] = Util::convertMicrotime($startTime);
0 ignored issues
show
Bug introduced by
It seems like $startTime can also be of type string; however, parameter $start of Cecil\Util::convertMicrotime() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

289
        $this->metrics['total']['duration'] = Util::convertMicrotime(/** @scrutinizer ignore-type */ $startTime);
Loading history...
290 1
        $this->metrics['total']['memory']   = Util::convertMemory(memory_get_usage() - $startMemory);
291 1
        $this->getLogger()->notice(\sprintf('Built in %s (%s)', $this->metrics['total']['duration'], $this->metrics['total']['memory']));
292
293 1
        return $this;
294
    }
295
296
    /**
297
     * Returns current build ID.
298
     */
299 1
    public function getBuildId(): string
300
    {
301 1
        return $this->buildId;
302
    }
303
304
    /**
305
     * Set configuration.
306
     */
307 1
    public function setConfig(array|Config $config): self
308
    {
309 1
        if (\is_array($config)) {
310 1
            $config = new Config($config);
311
        }
312 1
        if ($this->config !== $config) {
313 1
            $this->config = $config;
314
        }
315
316
        // import themes configuration
317 1
        $this->importThemesConfig();
318
        // autoloads local extensions
319 1
        Util::autoload($this, 'extensions');
320
321 1
        return $this;
322
    }
323
324
    /**
325
     * Returns configuration.
326
     */
327 1
    public function getConfig(): Config
328
    {
329 1
        if ($this->config === null) {
330
            $this->config = new Config();
331
        }
332
333 1
        return $this->config;
334
    }
335
336
    /**
337
     * Config::setSourceDir() alias.
338
     */
339 1
    public function setSourceDir(string $sourceDir): self
340
    {
341 1
        $this->getConfig()->setSourceDir($sourceDir);
342
        // import themes configuration
343 1
        $this->importThemesConfig();
344
345 1
        return $this;
346
    }
347
348
    /**
349
     * Config::setDestinationDir() alias.
350
     */
351 1
    public function setDestinationDir(string $destinationDir): self
352
    {
353 1
        $this->getConfig()->setDestinationDir($destinationDir);
354
355 1
        return $this;
356
    }
357
358
    /**
359
     * Import themes configuration.
360
     */
361 1
    public function importThemesConfig(): void
362
    {
363 1
        foreach ((array) $this->config->get('theme') as $theme) {
364 1
            $this->config->import(Config::loadFile(Util::joinFile($this->config->getThemesPath(), $theme, 'config.yml'), true), Config::PRESERVE);
0 ignored issues
show
Bug introduced by
The method getThemesPath() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

364
            $this->config->import(Config::loadFile(Util::joinFile($this->config->/** @scrutinizer ignore-call */ getThemesPath(), $theme, 'config.yml'), true), Config::PRESERVE);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
365
        }
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371 1
    public function setLogger(LoggerInterface $logger): void
372
    {
373 1
        $this->logger = $logger;
374
    }
375
376
    /**
377
     * Returns the logger instance.
378
     */
379 1
    public function getLogger(): LoggerInterface
380
    {
381 1
        return $this->logger;
382
    }
383
384
    /**
385
     * Returns debug mode state.
386
     */
387 1
    public function isDebug(): bool
388
    {
389 1
        return (bool) $this->debug;
390
    }
391
392
    /**
393
     * Returns build options.
394
     */
395 1
    public function getBuildOptions(): array
396
    {
397 1
        return $this->options;
398
    }
399
400
    /**
401
     * Set collected pages files.
402
     */
403 1
    public function setPagesFiles(Finder $content): void
404
    {
405 1
        $this->content = $content;
406
    }
407
408
    /**
409
     * Returns pages files.
410
     */
411 1
    public function getPagesFiles(): ?Finder
412
    {
413 1
        return $this->content;
414
    }
415
416
    /**
417
     * Set collected data.
418
     */
419 1
    public function setData(array $data): void
420
    {
421 1
        $this->data = $data;
422
    }
423
424
    /**
425
     * Returns data collection.
426
     */
427 1
    public function getData(?string $language = null): array
428
    {
429 1
        if ($language) {
430 1
            if (empty($this->data[$language])) {
431
                // fallback to default language
432 1
                return $this->data[$this->config->getLanguageDefault()];
433
            }
434
435 1
            return $this->data[$language];
436
        }
437
438 1
        return $this->data;
439
    }
440
441
    /**
442
     * Set collected static files.
443
     */
444 1
    public function setStatic(array $static): void
445
    {
446 1
        $this->static = $static;
447
    }
448
449
    /**
450
     * Returns static files collection.
451
     */
452 1
    public function getStatic(): array
453
    {
454 1
        return $this->static;
455
    }
456
457
    /**
458
     * Set/update Pages collection.
459
     */
460 1
    public function setPages(PagesCollection $pages): void
461
    {
462 1
        $this->pages = $pages;
463
    }
464
465
    /**
466
     * Returns pages collection.
467
     */
468 1
    public function getPages(): ?PagesCollection
469
    {
470 1
        return $this->pages;
471
    }
472
473
    /**
474
     * Set assets path list.
475
     */
476
    public function setAssets(array $assets): void
477
    {
478
        $this->assets = $assets;
479
    }
480
481
    /**
482
     * Add an asset path to assets list.
483
     */
484 1
    public function addAsset(string $path): void
485
    {
486 1
        if (!\in_array($path, $this->assets, true)) {
487 1
            $this->assets[] = $path;
488
        }
489
    }
490
491
    /**
492
     * Returns list of assets path.
493
     */
494 1
    public function getAssets(): array
495
    {
496 1
        return $this->assets;
497
    }
498
499
    /**
500
     * Set menus collection.
501
     */
502 1
    public function setMenus(array $menus): void
503
    {
504 1
        $this->menus = $menus;
505
    }
506
507
    /**
508
     * Returns all menus, for a language.
509
     */
510 1
    public function getMenus(string $language): Collection\Menu\Collection
511
    {
512 1
        return $this->menus[$language];
513
    }
514
515
    /**
516
     * Set taxonomies collection.
517
     */
518 1
    public function setTaxonomies(array $taxonomies): void
519
    {
520 1
        $this->taxonomies = $taxonomies;
521
    }
522
523
    /**
524
     * Returns taxonomies collection, for a language.
525
     */
526 1
    public function getTaxonomies(string $language): ?Collection\Taxonomy\Collection
527
    {
528 1
        return $this->taxonomies[$language];
529
    }
530
531
    /**
532
     * Set renderer object.
533
     */
534 1
    public function setRenderer(Renderer\RendererInterface $renderer): void
535
    {
536 1
        $this->renderer = $renderer;
537
    }
538
539
    /**
540
     * Returns Renderer object.
541
     */
542 1
    public function getRenderer(): Renderer\RendererInterface
543
    {
544 1
        return $this->renderer;
545
    }
546
547
    /**
548
     * Returns metrics array.
549
     */
550
    public function getMetrics(): array
551
    {
552
        return $this->metrics;
553
    }
554
555
    /**
556
     * Returns application version.
557
     *
558
     * @throws RuntimeException
559
     */
560 1
    public static function getVersion(): string
561
    {
562 1
        if (!isset(self::$version)) {
563
            try {
564 1
                $filePath = Util\File::getRealPath('VERSION');
565
                $version = Util\File::fileGetContents($filePath);
566
                if ($version === false) {
567
                    throw new RuntimeException(\sprintf('Can\'t read content of "%s".', $filePath));
568
                }
569
                self::$version = trim($version);
570 1
            } catch (\Exception) {
571 1
                self::$version = self::VERSION;
572
            }
573
        }
574
575 1
        return self::$version;
576
    }
577
578
    /**
579
     * Log soft errors.
580
     */
581 1
    protected function checkErrors(): void
582
    {
583
        // baseurl is required in production
584 1
        if (empty(trim((string) $this->config->get('baseurl'), '/'))) {
585
            $this->getLogger()->error('`baseurl` configuration key is required in production.');
586
        }
587
    }
588
}
589