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

Cecilapp / Cecil / 13526188422

25 Feb 2025 04:33PM UTC coverage: 83.394%. First build
13526188422

Pull #2126

github

web-flow
Merge 31d66caa5 into add370c5c
Pull Request #2126: refactor: better Twig profile

8 of 12 new or added lines in 1 file covered. (66.67%)

2973 of 3565 relevant lines covered (83.39%)

0.83 hits per line

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

87.05
/src/Step/Pages/Render.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <arnaud@ligny.fr>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13

14
namespace Cecil\Step\Pages;
15

16
use Cecil\Builder;
17
use Cecil\Collection\Page\Collection;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Exception\RuntimeException;
20
use Cecil\Renderer\Config;
21
use Cecil\Renderer\Layout;
22
use Cecil\Renderer\Site;
23
use Cecil\Renderer\Twig;
24
use Cecil\Step\AbstractStep;
25
use Cecil\Util;
26

27
/**
28
 * Pages rendering.
29
 */
30
class Render extends AbstractStep
31
{
32
    /**
33
     * {@inheritdoc}
34
     */
35
    public function getName(): string
36
    {
37
        return 'Rendering pages';
1✔
38
    }
39

40
    /**
41
     * {@inheritdoc}
42
     */
43
    public function init(array $options): void
44
    {
45
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
1✔
46
            $message = \sprintf('"%s" is not a valid layouts directory', $this->config->getLayoutsPath());
×
47
            $this->builder->getLogger()->debug($message);
×
48
        }
49

50
        $this->canProcess = true;
1✔
51
    }
52

53
    /**
54
     * {@inheritdoc}
55
     *
56
     * @throws RuntimeException
57
     */
58
    public function process(): void
59
    {
60
        // prepares renderer
61
        $this->builder->setRenderer(new Twig($this->builder, $this->getAllLayoutsPaths()));
1✔
62

63
        // adds global variables
64
        $this->addGlobals();
1✔
65

66
        /** @var Collection $pages */
67
        $pages = $this->builder->getPages()
1✔
68
            // published only
1✔
69
            ->filter(function (Page $page) {
1✔
70
                return (bool) $page->getVariable('published');
1✔
71
            })
1✔
72
            // enrichs some variables
1✔
73
            ->map(function (Page $page) {
1✔
74
                $formats = $this->getOutputFormats($page);
1✔
75
                // output formats
76
                $page->setVariable('output', $formats);
1✔
77
                // alternates formats
78
                $page->setVariable('alternates', $this->getAlternates($formats));
1✔
79
                // translations
80
                $page->setVariable('translations', $this->getTranslations($page));
1✔
81

82
                return $page;
1✔
83
            });
1✔
84
        $total = \count($pages);
1✔
85

86
        // renders each page
87
        $count = 0;
1✔
88
        $postprocessors = [];
1✔
89
        foreach ($this->config->get('output.postprocessors') ?? [] as $name => $postprocessor) {
1✔
90
            try {
91
                if (!class_exists($postprocessor)) {
1✔
92
                    throw new RuntimeException(\sprintf('Class "%s" not found', $postprocessor));
1✔
93
                }
94
                $postprocessors[] = new $postprocessor($this->builder);
1✔
95
                $this->builder->getLogger()->debug(\sprintf('Output post processor "%s" loaded', $name));
1✔
96
            } catch (\Exception $e) {
1✔
97
                $this->builder->getLogger()->error(\sprintf('Unable to load output post processor "%s": %s', $name, $e->getMessage()));
1✔
98
            }
99
        }
100
        /** @var Page $page */
101
        foreach ($pages as $page) {
1✔
102
            $count++;
1✔
103
            $rendered = [];
1✔
104

105
            // l10n
106
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
1✔
107
            $locale = $this->config->getLanguageProperty('locale', $language);
1✔
108
            $this->builder->getRenderer()->setLocale($locale);
1✔
109

110
            // global site variables
111
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $language));
1✔
112

113
            // global config raw variables
114
            $this->builder->getRenderer()->addGlobal('config', new Config($this->builder, $language));
1✔
115

116
            // excluded format(s)?
117
            $formats = (array) $page->getVariable('output');
1✔
118
            foreach ($formats as $key => $format) {
1✔
119
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
1✔
120
                    // ie:
121
                    //   formats:
122
                    //     atom:
123
                    //       [...]
124
                    //       exclude: [paginated]
125
                    if (!\is_array($exclude)) {
1✔
126
                        $exclude = [$exclude];
×
127
                    }
128
                    foreach ($exclude as $variable) {
1✔
129
                        if ($page->hasVariable($variable)) {
1✔
130
                            unset($formats[$key]);
1✔
131
                        }
132
                    }
133
                }
134
            }
135

136
            // renders each output format
137
            foreach ($formats as $format) {
1✔
138
                // search for the template
139
                $layout = Layout::finder($page, $format, $this->config);
1✔
140
                // renders with Twig
141
                try {
142
                    $deprecations = [];
1✔
143
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
1✔
144
                        if (E_USER_DEPRECATED === $type) {
1✔
145
                            $deprecations[] = $msg;
1✔
146
                        }
147
                    });
1✔
148
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
1✔
149
                    foreach ($deprecations as $value) {
1✔
150
                        $this->builder->getLogger()->warning($value);
1✔
151
                    }
152
                    foreach ($postprocessors as $postprocessor) {
1✔
153
                        $output = $postprocessor->process($page, $output, $format);
1✔
154
                    }
155
                    $rendered[$format] = [
1✔
156
                        'output'   => $output,
1✔
157
                        'template' => [
1✔
158
                            'scope' => $layout['scope'],
1✔
159
                            'file'  => $layout['file'],
1✔
160
                        ],
1✔
161
                    ];
1✔
162
                    $page->addRendered($rendered);
1✔
163
                } catch (\Twig\Error\Error $e) {
×
164
                    $template = !empty($e->getSourceContext()->getPath()) ? $e->getSourceContext()->getPath() : $e->getSourceContext()->getName();
×
165
                    throw new RuntimeException(\sprintf(
×
166
                        'Template "%s%s" (page: %s): %s',
×
167
                        $template,
×
168
                        $e->getTemplateLine() >= 0 ? \sprintf(':%s', $e->getTemplateLine()) : '',
×
169
                        $page->getId(),
×
170
                        $e->getMessage()
×
171
                    ));
×
NEW
172
                } catch (\Exception $e) {
×
NEW
173
                    throw new RuntimeException($e->getMessage());
×
174
                }
175
            }
176
            $this->builder->getPages()->replace($page->getId(), $page);
1✔
177

178
            $templates = array_column($rendered, 'template');
1✔
179
            $message = \sprintf(
1✔
180
                'Page "%s" rendered with [%s]',
1✔
181
                $page->getId() ?: 'index',
1✔
182
                Util\Str::combineArrayToString($templates, 'scope', 'file')
1✔
183
            );
1✔
184
            $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]);
1✔
185
        }
186
        // profiler
187
        if ($this->builder->isDebug()) {
1✔
188
            try {
189
                // HTML
190
                $htmlDumper = new \Twig\Profiler\Dumper\HtmlDumper();
1✔
191
                $profileHtmlFile = Util::joinFile($this->config->getDestinationDir(), '.debug/twig_profile.html');
1✔
192
                Util\File::getFS()->dumpFile($profileHtmlFile, $htmlDumper->dump($this->builder->getRenderer()->getDebugProfile()));
1✔
193
                // TXT
194
                $textDumper = new \Twig\Profiler\Dumper\TextDumper();
1✔
195
                $profileTextFile = Util::joinFile($this->config->getDestinationDir(), '.debug/twig_profile.txt');
1✔
196
                Util\File::getFS()->dumpFile($profileTextFile, $textDumper->dump($this->builder->getRenderer()->getDebugProfile()));
1✔
197
                // log
198
                $this->builder->getLogger()->debug(\sprintf('Twig profile dumped in "%s"', Util::joinFile($this->config->getDestinationDir(), '.debug/')));
1✔
NEW
199
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
×
NEW
200
                throw new RuntimeException($e->getMessage());
×
201
            }
202
        }
203
    }
204

205
    /**
206
     * Returns an array of layouts directories.
207
     */
208
    protected function getAllLayoutsPaths(): array
209
    {
210
        $paths = [];
1✔
211

212
        // layouts/
213
        if (is_dir($this->config->getLayoutsPath())) {
1✔
214
            $paths[] = $this->config->getLayoutsPath();
1✔
215
        }
216
        // <theme>/layouts/
217
        if ($this->config->hasTheme()) {
1✔
218
            foreach ($this->config->getTheme() ?? [] as $theme) {
1✔
219
                $paths[] = $this->config->getThemeDirPath($theme);
1✔
220
            }
221
        }
222
        // resources/layouts/
223
        if (is_dir($this->config->getLayoutsInternalPath())) {
1✔
224
            $paths[] = $this->config->getLayoutsInternalPath();
1✔
225
        }
226

227
        return $paths;
1✔
228
    }
229

230
    /**
231
     * Adds global variables.
232
     */
233
    protected function addGlobals()
234
    {
235
        $this->builder->getRenderer()->addGlobal('cecil', [
1✔
236
            'url'       => \sprintf('https://cecil.app/#%s', Builder::getVersion()),
1✔
237
            'version'   => Builder::getVersion(),
1✔
238
            'poweredby' => \sprintf('Cecil v%s', Builder::getVersion()),
1✔
239
        ]);
1✔
240
    }
241

242
    /**
243
     * Get available output formats.
244
     *
245
     * @throws RuntimeException
246
     */
247
    protected function getOutputFormats(Page $page): array
248
    {
249
        // Get page output format(s) if defined.
250
        // ie:
251
        // ```yaml
252
        // output: txt
253
        // ```
254
        if ($page->getVariable('output')) {
1✔
255
            $formats = $page->getVariable('output');
1✔
256
            if (!\is_array($formats)) {
1✔
257
                $formats = [$formats];
1✔
258
            }
259

260
            return $formats;
1✔
261
        }
262

263
        // Get available output formats for the page type.
264
        // ie:
265
        // ```yaml
266
        // page: [html, json]
267
        // ```
268
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
1✔
269
        if (empty($formats)) {
1✔
270
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
×
271
        }
272
        if (!\is_array($formats)) {
1✔
273
            $formats = [$formats];
×
274
        }
275

276
        return array_unique($formats);
1✔
277
    }
278

279
    /**
280
     * Get alternates.
281
     */
282
    protected function getAlternates(array $formats): array
283
    {
284
        $alternates = [];
1✔
285

286
        if (\count($formats) > 1 || \in_array('html', $formats)) {
1✔
287
            foreach ($formats as $format) {
1✔
288
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
1✔
289
                $alternates[] = [
1✔
290
                    'rel'    => $rel,
1✔
291
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
1✔
292
                    'title'  => strtoupper($format),
1✔
293
                    'format' => $format,
1✔
294
                ];
1✔
295
            }
296
        }
297

298
        return $alternates;
1✔
299
    }
300

301
    /**
302
     * Returns the collection of translated pages for a given page.
303
     */
304
    protected function getTranslations(Page $refPage): \Cecil\Collection\Page\Collection
305
    {
306
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
1✔
307
            return $page->getId() !== $refPage->getId()
1✔
308
                && $page->getVariable('langref') == $refPage->getVariable('langref')
1✔
309
                && $page->getType() == $refPage->getType()
1✔
310
                && !empty($page->getVariable('published'))
1✔
311
                && !$page->getVariable('paginated');
1✔
312
        });
1✔
313

314
        return $pages;
1✔
315
    }
316
}
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