• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 21676346600

04 Feb 2026 02:57PM UTC coverage: 82.378% (-0.2%) from 82.577%
21676346600

push

github

ArnaudLigny
Persist assets to temp file in debug mode

When running in debug mode, append asset paths to a temporary file (assets-<buildId>.txt) under the configured destination/TMP_DIR in addToAssetsList, and read that file in getAssetsList. If the file read fails, getAssetsList returns an empty array. Non-debug behavior (using the in-memory $assets array) is unchanged. This ensures the assets list is persisted to disk for debug builds.

14 of 15 new or added lines in 1 file covered. (93.33%)

40 existing lines in 4 files now uncovered.

3319 of 4029 relevant lines covered (82.38%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.57
/src/Builder.php
1
<?php
2

3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <arnaud@ligny.fr>
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
     * Temporary directory name.
90
     */
91
    public const TMP_DIR = '.cecil';
92

93
    /**
94
     * Configuration object.
95
     * This object holds all the configuration settings for the build process.
96
     * It can be set to an array or a Config instance.
97
     * @var Config|array|null
98
     * @see \Cecil\Config
99
     */
100
    protected $config;
101
    /**
102
     * Logger instance.
103
     * This logger is used to log messages during the build process.
104
     * It can be set to any PSR-3 compliant logger.
105
     * @var LoggerInterface
106
     * @see \Psr\Log\LoggerInterface
107
     * */
108
    protected $logger;
109
    /**
110
     * Debug mode state.
111
     * If true, debug messages are logged.
112
     * @var bool
113
     */
114
    protected $debug = false;
115
    /**
116
     * Build options.
117
     * These options can be passed to the build() method to customize the build process.
118
     * @var array
119
     * @see \Cecil\Builder::OPTIONS
120
     * @see \Cecil\Builder::build()
121
     */
122
    protected $options = [];
123
    /**
124
     * Content files collection.
125
     * This is a Finder instance that collects all the content files (pages, posts, etc.) from the source directory.
126
     * @var Finder
127
     */
128
    protected $content;
129
    /**
130
     * Data collection.
131
     * This is an associative array that holds data loaded from YAML files in the data directory.
132
     * @var array
133
     */
134
    protected $data = [];
135
    /**
136
     * Static files collection.
137
     * This is an associative array that holds static files (like images, CSS, JS) that are copied to the destination directory.
138
     * @var array
139
     */
140
    protected $static = [];
141
    /**
142
     * Pages collection.
143
     * This is a collection of pages that have been processed and are ready for rendering.
144
     * It is an instance of PagesCollection, which is a custom collection class for managing pages.
145
     * @var PagesCollection
146
     */
147
    protected $pages;
148
    /**
149
     * Assets path collection.
150
     * This is an array that holds paths to assets (like CSS, JS, images) that are used in the build process.
151
     * It is used to keep track of assets that need to be processed or copied.
152
     * It can be set to an array of paths or updated with new asset paths.
153
     * @var array
154
     */
155
    protected $assets = [];
156
    /**
157
     * Menus collection.
158
     * This is an associative array that holds menus for different languages.
159
     * Each key is a language code, and the value is a Collection\Menu\Collection instance
160
     * that contains the menu items for that language.
161
     * It is used to manage navigation menus across different languages in the website.
162
     * @var array
163
     * @see \Cecil\Collection\Menu\Collection
164
     */
165
    protected $menus;
166
    /**
167
     * Taxonomies collection.
168
     * This is an associative array that holds taxonomies for different languages.
169
     * Each key is a language code, and the value is a Collection\Taxonomy\Collection instance
170
     * that contains the taxonomy terms for that language.
171
     * It is used to manage taxonomies (like categories, tags) across different languages in the website.
172
     * @var array
173
     * @see \Cecil\Collection\Taxonomy\Collection
174
     */
175
    protected $taxonomies;
176
    /**
177
     * Renderer.
178
     * This is an instance of Renderer\Twig that is responsible for rendering pages.
179
     * It handles the rendering of templates and the application of data to those templates.
180
     * @var Renderer\Twig
181
     */
182
    protected $renderer;
183
    /**
184
     * Generators manager.
185
     * This is an instance of GeneratorManager that manages all the generators used in the build process.
186
     * Generators are used to create dynamic content or perform specific tasks during the build.
187
     * It allows for the registration and execution of various generators that can extend the functionality of the build process.
188
     * @var GeneratorManager
189
     */
190
    protected $generatorManager;
191
    /**
192
     * Build metrics.
193
     * This array holds metrics about the build process, such as duration and memory usage for each step.
194
     * It is used to track the performance of the build and can be useful for debugging and optimization.
195
     * @var array
196
     */
197
    protected $metrics = [];
198
    /**
199
     * Application version.
200
     * @var string
201
     */
202
    protected static $version;
203
    /**
204
     * Current build ID.
205
     * This is a unique identifier for the current build process.
206
     * It is generated based on the current date and time when the build starts.
207
     * It can be used to track builds, especially in environments where multiple builds may occur.
208
     * @var string
209
     * @see \Cecil\Builder::build()
210
     */
211
    protected static $buildId;
212

213
    /**
214
     * @param Config|array|null    $config
215
     * @param LoggerInterface|null $logger
216
     */
217
    public function __construct($config = null, ?LoggerInterface $logger = null)
218
    {
219
        // init and set config
220
        $this->config = new Config();
1✔
221
        if ($config !== null) {
1✔
222
            $this->setConfig($config);
1✔
223
        }
224
        // debug mode?
225
        if (getenv('CECIL_DEBUG') == 'true' || $this->getConfig()->isEnabled('debug')) {
1✔
226
            $this->debug = true;
1✔
227
        }
228
        // set logger
229
        if ($logger === null) {
1✔
UNCOV
230
            $logger = new PrintLogger(self::VERBOSITY_VERBOSE);
×
231
        }
232
        $this->setLogger($logger);
1✔
233
    }
234

235
    /**
236
     * Creates a new Builder instance.
237
     */
238
    public static function create(): self
239
    {
240
        $class = new \ReflectionClass(\get_called_class());
1✔
241

242
        return $class->newInstanceArgs(\func_get_args());
1✔
243
    }
244

245
    /**
246
     * Builds a new website.
247
     * This method processes the build steps in order, collects content, data, static files,
248
     * generates pages, renders them, and saves the output to the destination directory.
249
     * It also collects metrics about the build process, such as duration and memory usage.
250
     * @param array<self::OPTIONS> $options
251
     * @see \Cecil\Builder::OPTIONS
252
     */
253
    public function build(array $options): self
254
    {
255
        // set start script time and memory usage
256
        $startTime = microtime(true);
1✔
257
        $startMemory = memory_get_usage();
1✔
258

259
        // checks soft errors
260
        $this->checkErrors();
1✔
261

262
        // merge options with defaults
263
        $this->options = array_merge(self::OPTIONS, $options);
1✔
264

265
        // set build ID
266
        self::$buildId = hash('adler32', date('YmdHis') . self::$version);
1✔
267

268
        // process each step
269
        $steps = [];
1✔
270
        // init...
271
        foreach (self::STEPS as $step) {
1✔
272
            $stepObject = new $step($this);
1✔
273
            $stepObject->init($this->options);
1✔
274
            if ($stepObject->canProcess()) {
1✔
275
                $steps[] = $stepObject;
1✔
276
            }
277
        }
278
        // ...and process
279
        $stepNumber = 0;
1✔
280
        $stepsTotal = \count($steps);
1✔
281
        foreach ($steps as $step) {
1✔
282
            $stepNumber++;
1✔
283
            $this->getLogger()->notice($step->getName(), ['step' => [$stepNumber, $stepsTotal]]);
1✔
284
            $stepStartTime = microtime(true);
1✔
285
            $stepStartMemory = memory_get_usage();
1✔
286
            $step->process();
1✔
287
            // step duration and memory usage
288
            $this->metrics['steps'][$stepNumber]['name'] = $step->getName();
1✔
289
            $this->metrics['steps'][$stepNumber]['duration'] = Util::convertMicrotime(/** @scrutinizer ignore-type */ $stepStartTime);
1✔
290
            $this->metrics['steps'][$stepNumber]['memory']   = Util::convertMemory(memory_get_usage() - $stepStartMemory);
1✔
291
            $this->getLogger()->info(\sprintf(
1✔
292
                '%s done in %s (%s)',
1✔
293
                $this->metrics['steps'][$stepNumber]['name'],
1✔
294
                $this->metrics['steps'][$stepNumber]['duration'],
1✔
295
                $this->metrics['steps'][$stepNumber]['memory']
1✔
296
            ));
1✔
297
        }
298
        // build duration and memory usage
299
        $this->metrics['total']['duration'] = Util::convertMicrotime(/** @scrutinizer ignore-type */ $startTime);
1✔
300
        $this->metrics['total']['memory']   = Util::convertMemory(memory_get_usage() - $startMemory);
1✔
301
        $this->getLogger()->notice(\sprintf('Built in %s (%s)', $this->metrics['total']['duration'], $this->metrics['total']['memory']));
1✔
302

303
        return $this;
1✔
304
    }
305

306
    /**
307
     * Set configuration.
308
     */
309
    public function setConfig(array|Config $config): self
310
    {
311
        if (\is_array($config)) {
1✔
312
            $config = new Config($config);
1✔
313
        }
314
        if ($this->config !== $config) {
1✔
315
            $this->config = $config;
1✔
316
        }
317

318
        // import themes configuration
319
        $this->importThemesConfig();
1✔
320
        // autoloads local extensions
321
        Util::autoload($this, 'extensions');
1✔
322

323
        return $this;
1✔
324
    }
325

326
    /**
327
     * Returns configuration.
328
     */
329
    public function getConfig(): Config
330
    {
331
        if ($this->config === null) {
1✔
UNCOV
332
            $this->config = new Config();
×
333
        }
334

335
        return $this->config;
1✔
336
    }
337

338
    /**
339
     * Config::setSourceDir() alias.
340
     */
341
    public function setSourceDir(string $sourceDir): self
342
    {
343
        $this->getConfig()->setSourceDir($sourceDir);
1✔
344
        // import themes configuration
345
        $this->importThemesConfig();
1✔
346

347
        return $this;
1✔
348
    }
349

350
    /**
351
     * Config::setDestinationDir() alias.
352
     */
353
    public function setDestinationDir(string $destinationDir): self
354
    {
355
        $this->getConfig()->setDestinationDir($destinationDir);
1✔
356

357
        return $this;
1✔
358
    }
359

360
    /**
361
     * Import themes configuration.
362
     */
363
    public function importThemesConfig(): void
364
    {
365
        foreach ((array) $this->getConfig()->get('theme') as $theme) {
1✔
366
            $this->getConfig()->import(
1✔
367
                Config::loadFile(Util::joinFile($this->getConfig()->getThemesPath(), $theme, 'config.yml'), true),
1✔
368
                Config::IMPORT_PRESERVE
1✔
369
            );
1✔
370
        }
371
    }
372

373
    /**
374
     * {@inheritdoc}
375
     */
376
    public function setLogger(LoggerInterface $logger): void
377
    {
378
        $this->logger = $logger;
1✔
379
    }
380

381
    /**
382
     * Returns the logger instance.
383
     */
384
    public function getLogger(): LoggerInterface
385
    {
386
        return $this->logger;
1✔
387
    }
388

389
    /**
390
     * Returns debug mode state.
391
     */
392
    public function isDebug(): bool
393
    {
394
        return (bool) $this->debug;
1✔
395
    }
396

397
    /**
398
     * Returns build options.
399
     */
400
    public function getBuildOptions(): array
401
    {
402
        return $this->options;
1✔
403
    }
404

405
    /**
406
     * Set collected pages files.
407
     */
408
    public function setPagesFiles(Finder $content): void
409
    {
410
        $this->content = $content;
1✔
411
    }
412

413
    /**
414
     * Returns pages files.
415
     */
416
    public function getPagesFiles(): ?Finder
417
    {
418
        return $this->content;
1✔
419
    }
420

421
    /**
422
     * Set collected data.
423
     */
424
    public function setData(array $data): void
425
    {
426
        $this->data = $data;
1✔
427
    }
428

429
    /**
430
     * Returns data collection.
431
     */
432
    public function getData(?string $language = null): array
433
    {
434
        if ($language) {
1✔
435
            if (empty($this->data[$language])) {
1✔
436
                // fallback to default language
437
                return $this->data[$this->getConfig()->getLanguageDefault()];
1✔
438
            }
439

440
            return $this->data[$language];
1✔
441
        }
442

443
        return $this->data;
1✔
444
    }
445

446
    /**
447
     * Set collected static files.
448
     */
449
    public function setStatic(array $static): void
450
    {
451
        $this->static = $static;
1✔
452
    }
453

454
    /**
455
     * Returns static files collection.
456
     */
457
    public function getStatic(): array
458
    {
459
        return $this->static;
1✔
460
    }
461

462
    /**
463
     * Set/update Pages collection.
464
     */
465
    public function setPages(PagesCollection $pages): void
466
    {
467
        $this->pages = $pages;
1✔
468
    }
469

470
    /**
471
     * Returns pages collection.
472
     */
473
    public function getPages(): ?PagesCollection
474
    {
475
        return $this->pages;
1✔
476
    }
477

478
    /**
479
     * Add an asset path to assets list.
480
     */
481
    public function addToAssetsList(string $path): void
482
    {
483
        if ($this->isDebug()) {
1✔
484
            file_put_contents(
1✔
485
                Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR, 'assets-' . Builder::getBuildId() . '.txt'),
1✔
486
                $path . PHP_EOL,
1✔
487
                FILE_APPEND | LOCK_EX
1✔
488
            );
1✔
489
            return;
1✔
490
        }
UNCOV
491
        if (!\in_array($path, $this->assets, true)) {
×
UNCOV
492
            $this->assets[] = $path;
×
493
        }
494
    }
495

496
    /**
497
     * Returns list of assets path.
498
     */
499
    public function getAssetsList(): array
500
    {
501
        if ($this->isDebug()) {
1✔
502
            $assets = file(
1✔
503
                Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR, 'assets-' . Builder::getBuildId() . '.txt'),
1✔
504
                FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
1✔
505
            );
1✔
506
            if ($assets === false) {
1✔
507
                return [];
1✔
508
            }
509

NEW
510
            return $assets;
×
511
        }
UNCOV
512
        return $this->assets;
×
513
    }
514

515
    /**
516
     * Set menus collection.
517
     */
518
    public function setMenus(array $menus): void
519
    {
520
        $this->menus = $menus;
1✔
521
    }
522

523
    /**
524
     * Returns all menus, for a language.
525
     */
526
    public function getMenus(string $language): Collection\Menu\Collection
527
    {
528
        return $this->menus[$language];
1✔
529
    }
530

531
    /**
532
     * Set taxonomies collection.
533
     */
534
    public function setTaxonomies(array $taxonomies): void
535
    {
536
        $this->taxonomies = $taxonomies;
1✔
537
    }
538

539
    /**
540
     * Returns taxonomies collection, for a language.
541
     */
542
    public function getTaxonomies(string $language): ?Collection\Taxonomy\Collection
543
    {
544
        return $this->taxonomies[$language];
1✔
545
    }
546

547
    /**
548
     * Set renderer object.
549
     */
550
    public function setRenderer(Renderer\Twig $renderer): void
551
    {
552
        $this->renderer = $renderer;
1✔
553
    }
554

555
    /**
556
     * Returns Renderer object.
557
     */
558
    public function getRenderer(): Renderer\Twig
559
    {
560
        return $this->renderer;
1✔
561
    }
562

563
    /**
564
     * Returns metrics array.
565
     */
566
    public function getMetrics(): array
567
    {
UNCOV
568
        return $this->metrics;
×
569
    }
570

571
    /**
572
     * Returns application version.
573
     *
574
     * @throws RuntimeException
575
     */
576
    public static function getVersion(): string
577
    {
578
        if (!isset(self::$version)) {
1✔
579
            try {
580
                $filePath = Util\File::getRealPath('VERSION');
1✔
581
                $version = Util\File::fileGetContents($filePath);
×
UNCOV
582
                if ($version === false) {
×
UNCOV
583
                    throw new RuntimeException(\sprintf('Unable to read content of "%s".', $filePath));
×
584
                }
UNCOV
585
                self::$version = trim($version);
×
586
            } catch (\Exception) {
1✔
587
                self::$version = self::VERSION;
1✔
588
            }
589
        }
590

591
        return self::$version;
1✔
592
    }
593

594
    /**
595
     * Returns current build ID.
596
     */
597
    public static function getBuildId(): string
598
    {
599
        return self::$buildId;
1✔
600
    }
601

602
    /**
603
     * Log soft errors.
604
     */
605
    protected function checkErrors(): void
606
    {
607
        // baseurl is required in production
608
        if (empty(trim((string) $this->getConfig()->get('baseurl'), '/'))) {
1✔
UNCOV
609
            $this->getLogger()->error('`baseurl` configuration key is required in production.');
×
610
        }
611
    }
612
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc