Builder::build()   A
last analyzed

Complexity

Conditions 5
Paths 10

Size

Total Lines 64
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 5.0909

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 37
c 2
b 0
f 0
nc 10
nop 1
dl 0
loc 64
ccs 33
cts 39
cp 0.8462
crap 5.0909
rs 9.0168

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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