Issues (23)

Branch: php-doc

src/Builder.php (1 issue)

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

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