Builder::setPages()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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\Container\ContainerFactory;
18
use Cecil\Exception\RuntimeException;
19
use Cecil\Generator\GeneratorManager;
20
use Cecil\Logger\PrintLogger;
21
use DI\Container;
22
use Psr\Log\LoggerAwareInterface;
23
use Psr\Log\LoggerInterface;
24
use Symfony\Component\Finder\Finder;
25
26
/**
27
 * The main Cecil builder class.
28
 *
29
 * This class is responsible for building the website by processing various steps,
30
 * managing configuration, and handling content, data, static files, pages, assets,
31
 * menus, taxonomies, and rendering.
32
 * It also provides methods for logging, debugging, and managing build metrics.
33
 *
34
 * ```php
35
 * $config = [
36
 *   'title'   => "My website",
37
 *   'baseurl' => 'https://domain.tld/',
38
 * ];
39
 * Builder::create($config)->build();
40
 * ```
41
 */
42
class Builder implements LoggerAwareInterface
43
{
44
    public const VERSION = '8.x-dev';
45
    public const VERBOSITY_QUIET = -1;
46
    public const VERBOSITY_NORMAL = 0;
47
    public const VERBOSITY_VERBOSE = 1;
48
    public const VERBOSITY_DEBUG = 2;
49
    /**
50
     * Default options for the build process.
51
     * These options can be overridden when calling the build() method.
52
     * - 'drafts': if true, builds drafts too (default: false)
53
     * - 'dry-run': if true, generated files are not saved (default: false)
54
     * - 'page': if specified, only this page is processed (default: '')
55
     * - 'render-subset': limits the render step to a specific subset (default: '')
56
     * @var array<string, bool|string>
57
     * @see \Cecil\Builder::build()
58
     */
59
    public const OPTIONS = [
60
        'drafts'  => false,
61
        'dry-run' => false,
62
        'page'    => '',
63
        'render-subset' => '',
64
    ];
65
    /**
66
     * Steps processed by build(), in order.
67
     * These steps are executed sequentially to build the website.
68
     * Each step is a class that implements the StepInterface.
69
     * @var array<string>
70
     * @see \Cecil\Step\StepInterface
71
     */
72
    public const STEPS = [
73
        'Cecil\Step\Pages\Load',
74
        'Cecil\Step\Data\Load',
75
        'Cecil\Step\StaticFiles\Load',
76
        'Cecil\Step\Pages\Create',
77
        'Cecil\Step\Pages\Convert',
78
        'Cecil\Step\Taxonomies\Create',
79
        'Cecil\Step\Pages\Generate',
80
        'Cecil\Step\Menus\Create',
81
        'Cecil\Step\StaticFiles\Copy',
82
        'Cecil\Step\Pages\Render',
83
        'Cecil\Step\Pages\Save',
84
        'Cecil\Step\Assets\Save',
85
        'Cecil\Step\Optimize\Html',
86
        'Cecil\Step\Optimize\Css',
87
        'Cecil\Step\Optimize\Js',
88
        'Cecil\Step\Optimize\Images',
89
    ];
90
91
    /**
92
     * Configuration object.
93
     * This object holds all the configuration settings for the build process.
94
     * It can be set to an array or a Config instance.
95
     * @var Config|array|null
96
     * @see \Cecil\Config
97
     */
98
    protected $config;
99
    /**
100
     * Logger instance.
101
     * This logger is used to log messages during the build process.
102
     * It can be set to any PSR-3 compliant logger.
103
     * @var LoggerInterface
104
     * @see \Psr\Log\LoggerInterface
105
     * */
106
    protected $logger;
107
    /**
108
     * Debug mode state.
109
     * If true, debug messages are logged.
110
     * @var bool
111
     */
112
    protected $debug = false;
113
    /**
114
     * Build options.
115
     * These options can be passed to the build() method to customize the build process.
116
     * @var array
117
     * @see \Cecil\Builder::OPTIONS
118
     * @see \Cecil\Builder::build()
119
     */
120
    protected $options = [];
121
    /**
122
     * Content files collection.
123
     * This is a Finder instance that collects all the content files (pages, posts, etc.) from the source directory.
124
     * @var Finder
125
     */
126
    protected $content;
127
    /**
128
     * Data collection.
129
     * This is an associative array that holds data loaded from YAML files in the data directory.
130
     * @var array
131
     */
132
    protected $data = [];
133
    /**
134
     * Static files collection.
135
     * This is an associative array that holds static files (like images, CSS, JS) that are copied to the destination directory.
136
     * @var array
137
     */
138
    protected $static = [];
139
    /**
140
     * Pages collection.
141
     * This is a collection of pages that have been processed and are ready for rendering.
142
     * It is an instance of PagesCollection, which is a custom collection class for managing pages.
143
     * @var PagesCollection
144
     */
145
    protected $pages;
146
    /**
147
     * Assets path collection.
148
     * This is an array that holds paths to assets (like CSS, JS, images) that are used in the build process.
149
     * It is used to keep track of assets that need to be processed or copied.
150
     * It can be set to an array of paths or updated with new asset paths.
151
     * @var array
152
     */
153
    protected $assets = [];
154
    /**
155
     * Menus collection.
156
     * This is an associative array that holds menus for different languages.
157
     * Each key is a language code, and the value is a Collection\Menu\Collection instance
158
     * that contains the menu items for that language.
159
     * It is used to manage navigation menus across different languages in the website.
160
     * @var array
161
     * @see \Cecil\Collection\Menu\Collection
162
     */
163
    protected $menus;
164
    /**
165
     * Taxonomies collection.
166
     * This is an associative array that holds taxonomies for different languages.
167
     * Each key is a language code, and the value is a Collection\Taxonomy\Collection instance
168
     * that contains the taxonomy terms for that language.
169
     * It is used to manage taxonomies (like categories, tags) across different languages in the website.
170
     * @var array
171
     * @see \Cecil\Collection\Taxonomy\Collection
172
     */
173
    protected $taxonomies;
174
    /**
175
     * Renderer.
176
     * This is an instance of Renderer\Twig that is responsible for rendering pages.
177
     * It handles the rendering of templates and the application of data to those templates.
178
     * @var Renderer\Twig
179
     */
180
    protected $renderer;
181
    /**
182
     * Generators manager.
183
     * This is an instance of GeneratorManager that manages all the generators used in the build process.
184
     * Generators are used to create dynamic content or perform specific tasks during the build.
185
     * It allows for the registration and execution of various generators that can extend the functionality of the build process.
186
     * @var GeneratorManager
187
     */
188
    protected $generatorManager;
189
    /**
190
     * Application version.
191
     * @var string
192
     */
193
    protected static $version;
194
    /**
195
     * Build metrics.
196
     * This array holds metrics about the build process, such as duration and memory usage for each step.
197
     * It is used to track the performance of the build and can be useful for debugging and optimization.
198
     * @var array
199
     */
200
    protected $metrics = [];
201
    /**
202
     * Current build ID.
203
     * This is a unique identifier for the current build process.
204
     * It is generated based on the current date and time when the build starts.
205
     * It can be used to track builds, especially in environments where multiple builds may occur.
206
     * @var string
207
     * @see \Cecil\Builder::build()
208
     */
209
    protected $buildId;
210
    /**
211
     * Dependency injection container.
212
     * This container is used to manage dependencies and services throughout the application.
213
     * It allows for easier testing, better modularity, and cleaner separation of concerns.
214
     * @var Container
215
     */
216
    protected $container;
217
218
    /**
219
     * @param Config|array|null    $config
220
     * @param LoggerInterface|null $logger
221
     */
222
    public function __construct($config = null, ?LoggerInterface $logger = null)
223
    {
224
        // init and set config
225
        $this->config = new Config();
226
        if ($config !== null) {
227
            $this->setConfig($config);
228
        }
229
        // debug mode?
230
        if (getenv('CECIL_DEBUG') == 'true' || $this->getConfig()->isEnabled('debug')) {
231
            $this->debug = true;
232
        }
233
        // set logger
234
        if ($logger === null) {
235
            $logger = new PrintLogger(self::VERBOSITY_VERBOSE);
236
        }
237
        $this->setLogger($logger);
238
239
        // initialize DI container
240
        $this->container = ContainerFactory::create($this->config, $this->logger);
241
242
        // Inject Builder itself into the container for services that need it
243
        $this->container->set(Builder::class, $this);
244
    }
245
246
    /**
247
     * Creates a new Builder instance.
248
     */
249
    public static function create(): self
250
    {
251
        $class = new \ReflectionClass(\get_called_class());
252
253
        return $class->newInstanceArgs(\func_get_args());
254
    }
255
256
    /**
257
     * Builds a new website.
258
     * This method processes the build steps in order, collects content, data, static files,
259
     * generates pages, renders them, and saves the output to the destination directory.
260
     * It also collects metrics about the build process, such as duration and memory usage.
261
     * @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...
262
     * @see \Cecil\Builder::OPTIONS
263
     */
264
    public function build(array $options): self
265
    {
266
        // set start script time and memory usage
267
        $startTime = microtime(true);
268
        $startMemory = memory_get_usage();
269
270
        // checks soft errors
271
        $this->checkErrors();
272
273
        // merge options with defaults
274
        $this->options = array_merge(self::OPTIONS, $options);
275
276
        // set build ID
277
        $this->buildId = date('YmdHis');
278
279
        // process each step
280
        $steps = [];
281
        // init...
282
        foreach (self::STEPS as $step) {
283
            // Use DI container to create steps
284
            try {
285
                $stepObject = $this->container->get($step);
286
            } catch (\Exception $e) {
287
                // Fallback for steps not declared in the container
288
                $stepObject = new $step($this);
289
            }
290
            $stepObject->init($this->options);
291
            if ($stepObject->canProcess()) {
292
                $steps[] = $stepObject;
293
            }
294
        }
295
        // ...and process
296
        $stepNumber = 0;
297
        $stepsTotal = \count($steps);
298
        foreach ($steps as $step) {
299
            $stepNumber++;
300
            $this->getLogger()->notice($step->getName(), ['step' => [$stepNumber, $stepsTotal]]);
301
            $stepStartTime = microtime(true);
302
            $stepStartMemory = memory_get_usage();
303
            $step->process();
304
            // step duration and memory usage
305
            $this->metrics['steps'][$stepNumber]['name'] = $step->getName();
306
            $this->metrics['steps'][$stepNumber]['duration'] = Util::convertMicrotime(/** @scrutinizer ignore-type */ $stepStartTime);
307
            $this->metrics['steps'][$stepNumber]['memory']   = Util::convertMemory(memory_get_usage() - $stepStartMemory);
308
            $this->getLogger()->info(\sprintf(
309
                '%s done in %s (%s)',
310
                $this->metrics['steps'][$stepNumber]['name'],
311
                $this->metrics['steps'][$stepNumber]['duration'],
312
                $this->metrics['steps'][$stepNumber]['memory']
313
            ));
314
        }
315
        // build duration and memory usage
316
        $this->metrics['total']['duration'] = Util::convertMicrotime(/** @scrutinizer ignore-type */ $startTime);
317
        $this->metrics['total']['memory']   = Util::convertMemory(memory_get_usage() - $startMemory);
318
        $this->getLogger()->notice(\sprintf('Built in %s (%s)', $this->metrics['total']['duration'], $this->metrics['total']['memory']));
319
320
        return $this;
321
    }
322
323
    /**
324
     * Returns current build ID.
325
     */
326
    public function getBuildId(): string
327
    {
328
        return $this->buildId;
329
    }
330
331
    /**
332
     * Set configuration.
333
     */
334
    public function setConfig(array|Config $config): self
335
    {
336
        if (\is_array($config)) {
337
            $config = new Config($config);
338
        }
339
        if ($this->config !== $config) {
340
            $this->config = $config;
341
        }
342
343
        // import themes configuration
344
        $this->importThemesConfig();
345
        // autoloads local extensions
346
        Util::autoload($this, 'extensions');
347
348
        return $this;
349
    }
350
351
    /**
352
     * Returns configuration.
353
     */
354
    public function getConfig(): Config
355
    {
356
        if ($this->config === null) {
357
            $this->config = new Config();
358
        }
359
360
        return $this->config;
361
    }
362
363
    /**
364
     * Config::setSourceDir() alias.
365
     */
366
    public function setSourceDir(string $sourceDir): self
367
    {
368
        $this->getConfig()->setSourceDir($sourceDir);
369
        // import themes configuration
370
        $this->importThemesConfig();
371
372
        return $this;
373
    }
374
375
    /**
376
     * Config::setDestinationDir() alias.
377
     */
378
    public function setDestinationDir(string $destinationDir): self
379
    {
380
        $this->getConfig()->setDestinationDir($destinationDir);
381
382
        return $this;
383
    }
384
385
    /**
386
     * Import themes configuration.
387
     */
388
    public function importThemesConfig(): void
389
    {
390
        foreach ((array) $this->getConfig()->get('theme') as $theme) {
391
            $this->getConfig()->import(
392
                Config::loadFile(Util::joinFile($this->getConfig()->getThemesPath(), $theme, 'config.yml'), true),
393
                Config::IMPORT_PRESERVE
394
            );
395
        }
396
    }
397
398
    /**
399
     * {@inheritdoc}
400
     */
401
    public function setLogger(LoggerInterface $logger): void
402
    {
403
        $this->logger = $logger;
404
    }
405
406
    /**
407
     * Returns the logger instance.
408
     */
409
    public function getLogger(): LoggerInterface
410
    {
411
        return $this->logger;
412
    }
413
414
    /**
415
     * Returns debug mode state.
416
     */
417
    public function isDebug(): bool
418
    {
419
        return (bool) $this->debug;
420
    }
421
422
    /**
423
     * Returns build options.
424
     */
425
    public function getBuildOptions(): array
426
    {
427
        return $this->options;
428
    }
429
430
    /**
431
     * Set collected pages files.
432
     */
433
    public function setPagesFiles(Finder $content): void
434
    {
435
        $this->content = $content;
436
    }
437
438
    /**
439
     * Returns pages files.
440
     */
441
    public function getPagesFiles(): ?Finder
442
    {
443
        return $this->content;
444
    }
445
446
    /**
447
     * Set collected data.
448
     */
449
    public function setData(array $data): void
450
    {
451
        $this->data = $data;
452
    }
453
454
    /**
455
     * Returns data collection.
456
     */
457
    public function getData(?string $language = null): array
458
    {
459
        if ($language) {
460
            if (empty($this->data[$language])) {
461
                // fallback to default language
462
                return $this->data[$this->getConfig()->getLanguageDefault()];
463
            }
464
465
            return $this->data[$language];
466
        }
467
468
        return $this->data;
469
    }
470
471
    /**
472
     * Set collected static files.
473
     */
474
    public function setStatic(array $static): void
475
    {
476
        $this->static = $static;
477
    }
478
479
    /**
480
     * Returns static files collection.
481
     */
482
    public function getStatic(): array
483
    {
484
        return $this->static;
485
    }
486
487
    /**
488
     * Set/update Pages collection.
489
     */
490
    public function setPages(PagesCollection $pages): void
491
    {
492
        $this->pages = $pages;
493
    }
494
495
    /**
496
     * Returns pages collection.
497
     */
498
    public function getPages(): ?PagesCollection
499
    {
500
        return $this->pages;
501
    }
502
503
    /**
504
     * Set assets path list.
505
     */
506
    public function setAssets(array $assets): void
507
    {
508
        $this->assets = $assets;
509
    }
510
511
    /**
512
     * Add an asset path to assets list.
513
     */
514
    public function addAsset(string $path): void
515
    {
516
        if (!\in_array($path, $this->assets, true)) {
517
            $this->assets[] = $path;
518
        }
519
    }
520
521
    /**
522
     * Returns list of assets path.
523
     */
524
    public function getAssets(): array
525
    {
526
        return $this->assets;
527
    }
528
529
    /**
530
     * Set menus collection.
531
     */
532
    public function setMenus(array $menus): void
533
    {
534
        $this->menus = $menus;
535
    }
536
537
    /**
538
     * Returns all menus, for a language.
539
     */
540
    public function getMenus(string $language): Collection\Menu\Collection
541
    {
542
        return $this->menus[$language];
543
    }
544
545
    /**
546
     * Set taxonomies collection.
547
     */
548
    public function setTaxonomies(array $taxonomies): void
549
    {
550
        $this->taxonomies = $taxonomies;
551
    }
552
553
    /**
554
     * Returns taxonomies collection, for a language.
555
     */
556
    public function getTaxonomies(string $language): ?Collection\Taxonomy\Collection
557
    {
558
        return $this->taxonomies[$language];
559
    }
560
561
    /**
562
     * Set renderer object.
563
     */
564
    public function setRenderer(Renderer\Twig $renderer): void
565
    {
566
        $this->renderer = $renderer;
567
    }
568
569
    /**
570
     * Returns Renderer object.
571
     */
572
    public function getRenderer(): Renderer\Twig
573
    {
574
        return $this->renderer;
575
    }
576
577
    /**
578
     * Returns metrics array.
579
     */
580
    public function getMetrics(): array
581
    {
582
        return $this->metrics;
583
    }
584
585
    /**
586
     * Returns application version.
587
     *
588
     * @throws RuntimeException
589
     */
590
    public static function getVersion(): string
591
    {
592
        if (!isset(self::$version)) {
593
            try {
594
                $filePath = Util\File::getRealPath('VERSION');
595
                $version = Util\File::fileGetContents($filePath);
596
                if ($version === false) {
597
                    throw new RuntimeException(\sprintf('Unable to read content of "%s".', $filePath));
598
                }
599
                self::$version = trim($version);
600
            } catch (\Exception) {
601
                self::$version = self::VERSION;
602
            }
603
        }
604
605
        return self::$version;
606
    }
607
608
    /**
609
     * Gets a service from the DI container.
610
     * This method provides access to services managed by the dependency injection container.
611
     * @param string $id The service identifier (typically a class name)
612
     * @return mixed The resolved service instance
613
     */
614
    public function get(string $id): mixed
615
    {
616
        return $this->container->get($id);
617
    }
618
619
    /**
620
     * Gets the DI container instance.
621
     * @return Container The dependency injection container
622
     */
623
    public function getContainer(): Container
624
    {
625
        return $this->container;
626
    }
627
628
    /**
629
     * Log soft errors.
630
     */
631
    protected function checkErrors(): void
632
    {
633
        // baseurl is required in production
634
        if (empty(trim((string) $this->getConfig()->get('baseurl'), '/'))) {
635
            $this->getLogger()->error('`baseurl` configuration key is required in production.');
636
        }
637
    }
638
}
639