Issues (16)

Branch: master

src/Builder.php (2 issues)

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

297
        $this->metrics['total']['duration'] = Util::convertMicrotime(/** @scrutinizer ignore-type */ $startTime);
Loading history...
298 1
        $this->metrics['total']['memory']   = Util::convertMemory(memory_get_usage() - $startMemory);
299 1
        $this->getLogger()->notice(\sprintf('Built in %s (%s)', $this->metrics['total']['duration'], $this->metrics['total']['memory']));
300
301 1
        return $this;
302
    }
303
304
    /**
305
     * Returns current build ID.
306
     */
307 1
    public function getBuildId(): string
308
    {
309 1
        return $this->buildId;
310
    }
311
312
    /**
313
     * Set configuration.
314
     */
315 1
    public function setConfig(array|Config $config): self
316
    {
317 1
        if (\is_array($config)) {
318 1
            $config = new Config($config);
319
        }
320 1
        if ($this->config !== $config) {
321 1
            $this->config = $config;
322
        }
323
324
        // import themes configuration
325 1
        $this->importThemesConfig();
326
        // autoloads local extensions
327 1
        Util::autoload($this, 'extensions');
328
329 1
        return $this;
330
    }
331
332
    /**
333
     * Returns configuration.
334
     */
335 1
    public function getConfig(): Config
336
    {
337 1
        if ($this->config === null) {
338
            $this->config = new Config();
339
        }
340
341 1
        return $this->config;
342
    }
343
344
    /**
345
     * Config::setSourceDir() alias.
346
     */
347 1
    public function setSourceDir(string $sourceDir): self
348
    {
349 1
        $this->getConfig()->setSourceDir($sourceDir);
350
        // import themes configuration
351 1
        $this->importThemesConfig();
352
353 1
        return $this;
354
    }
355
356
    /**
357
     * Config::setDestinationDir() alias.
358
     */
359 1
    public function setDestinationDir(string $destinationDir): self
360
    {
361 1
        $this->getConfig()->setDestinationDir($destinationDir);
362
363 1
        return $this;
364
    }
365
366
    /**
367
     * Import themes configuration.
368
     */
369 1
    public function importThemesConfig(): void
370
    {
371 1
        foreach ((array) $this->getConfig()->get('theme') as $theme) {
372 1
            $this->getConfig()->import(
373 1
                Config::loadFile(Util::joinFile($this->getConfig()->getThemesPath(), $theme, 'config.yml'), true),
374 1
                Config::IMPORT_PRESERVE
375 1
            );
376
        }
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382 1
    public function setLogger(LoggerInterface $logger): void
383
    {
384 1
        $this->logger = $logger;
385
    }
386
387
    /**
388
     * Returns the logger instance.
389
     */
390 1
    public function getLogger(): LoggerInterface
391
    {
392 1
        return $this->logger;
393
    }
394
395
    /**
396
     * Returns debug mode state.
397
     */
398 1
    public function isDebug(): bool
399
    {
400 1
        return (bool) $this->debug;
401
    }
402
403
    /**
404
     * Returns build options.
405
     */
406 1
    public function getBuildOptions(): array
407
    {
408 1
        return $this->options;
409
    }
410
411
    /**
412
     * Set collected pages files.
413
     */
414 1
    public function setPagesFiles(Finder $content): void
415
    {
416 1
        $this->content = $content;
417
    }
418
419
    /**
420
     * Returns pages files.
421
     */
422 1
    public function getPagesFiles(): ?Finder
423
    {
424 1
        return $this->content;
425
    }
426
427
    /**
428
     * Set collected data.
429
     */
430 1
    public function setData(array $data): void
431
    {
432 1
        $this->data = $data;
433
    }
434
435
    /**
436
     * Returns data collection.
437
     */
438 1
    public function getData(?string $language = null): array
439
    {
440 1
        if ($language) {
441 1
            if (empty($this->data[$language])) {
442
                // fallback to default language
443 1
                return $this->data[$this->getConfig()->getLanguageDefault()];
444
            }
445
446 1
            return $this->data[$language];
447
        }
448
449 1
        return $this->data;
450
    }
451
452
    /**
453
     * Set collected static files.
454
     */
455 1
    public function setStatic(array $static): void
456
    {
457 1
        $this->static = $static;
458
    }
459
460
    /**
461
     * Returns static files collection.
462
     */
463 1
    public function getStatic(): array
464
    {
465 1
        return $this->static;
466
    }
467
468
    /**
469
     * Set/update Pages collection.
470
     */
471 1
    public function setPages(PagesCollection $pages): void
472
    {
473 1
        $this->pages = $pages;
474
    }
475
476
    /**
477
     * Returns pages collection.
478
     */
479 1
    public function getPages(): ?PagesCollection
480
    {
481 1
        return $this->pages;
482
    }
483
484
    /**
485
     * Set assets path list.
486
     */
487
    public function setAssets(array $assets): void
488
    {
489
        $this->assets = $assets;
490
    }
491
492
    /**
493
     * Add an asset path to assets list.
494
     */
495 1
    public function addAsset(string $path): void
496
    {
497 1
        if (!\in_array($path, $this->assets, true)) {
498 1
            $this->assets[] = $path;
499
        }
500
    }
501
502
    /**
503
     * Returns list of assets path.
504
     */
505 1
    public function getAssets(): array
506
    {
507 1
        return $this->assets;
508
    }
509
510
    /**
511
     * Set menus collection.
512
     */
513 1
    public function setMenus(array $menus): void
514
    {
515 1
        $this->menus = $menus;
516
    }
517
518
    /**
519
     * Returns all menus, for a language.
520
     */
521 1
    public function getMenus(string $language): Collection\Menu\Collection
522
    {
523 1
        return $this->menus[$language];
524
    }
525
526
    /**
527
     * Set taxonomies collection.
528
     */
529 1
    public function setTaxonomies(array $taxonomies): void
530
    {
531 1
        $this->taxonomies = $taxonomies;
532
    }
533
534
    /**
535
     * Returns taxonomies collection, for a language.
536
     */
537 1
    public function getTaxonomies(string $language): ?Collection\Taxonomy\Collection
538
    {
539 1
        return $this->taxonomies[$language];
540
    }
541
542
    /**
543
     * Set renderer object.
544
     */
545 1
    public function setRenderer(Renderer\RendererInterface $renderer): void
546
    {
547 1
        $this->renderer = $renderer;
548
    }
549
550
    /**
551
     * Returns Renderer object.
552
     */
553 1
    public function getRenderer(): Renderer\RendererInterface
554
    {
555 1
        return $this->renderer;
556
    }
557
558
    /**
559
     * Returns metrics array.
560
     */
561
    public function getMetrics(): array
562
    {
563
        return $this->metrics;
564
    }
565
566
    /**
567
     * Returns application version.
568
     *
569
     * @throws RuntimeException
570
     */
571 1
    public static function getVersion(): string
572
    {
573 1
        if (!isset(self::$version)) {
574
            try {
575 1
                $filePath = Util\File::getRealPath('VERSION');
576
                $version = Util\File::fileGetContents($filePath);
577
                if ($version === false) {
578
                    throw new RuntimeException(\sprintf('Can\'t read content of "%s".', $filePath));
579
                }
580
                self::$version = trim($version);
581 1
            } catch (\Exception) {
582 1
                self::$version = self::VERSION;
583
            }
584
        }
585
586 1
        return self::$version;
587
    }
588
589
    /**
590
     * Log soft errors.
591
     */
592 1
    protected function checkErrors(): void
593
    {
594
        // baseurl is required in production
595 1
        if (empty(trim((string) $this->getConfig()->get('baseurl'), '/'))) {
596
            $this->getLogger()->error('`baseurl` configuration key is required in production.');
597
        }
598
    }
599
}
600