Issues (9)

Branch: php-di

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