Render::process()   F
last analyzed

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 186
Code Lines 109

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 89
CRAP Score 40.5245

Importance

Changes 0
Metric Value
cc 29
eloc 109
nc 123318
nop 0
dl 0
loc 186
ccs 89
cts 117
cp 0.7607
crap 40.5245
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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\Step\Pages;
15
16
use Cecil\Builder;
17
use Cecil\Collection\Page\Collection;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Config;
20
use Cecil\Exception\ConfigException;
21
use Cecil\Exception\RuntimeException;
22
use Cecil\Renderer\Config as RendererConfig;
23
use Cecil\Renderer\Layout;
24
use Cecil\Renderer\Page as PageRenderer;
25
use Cecil\Renderer\Site;
26
use Cecil\Renderer\Twig;
27
use Cecil\Renderer\Twig\TwigFactory;
28
use Cecil\Step\AbstractStep;
29
use Cecil\Util;
30
use DI\Attribute\Inject;
31
use Psr\Log\LoggerInterface;
32
33
/**
34
 * Render step.
35
 *
36
 * This step is responsible for rendering pages using Twig templates.
37
 * It processes each page, applies the appropriate templates, and generates
38
 * the final output formats. It also handles subsets of pages if specified,
39
 * and adds global variables to the renderer. The rendered pages are then
40
 * stored in the builder's pages collection for further processing or output.
41
 */
42
class Render extends AbstractStep
43
{
44
    public const TMP_DIR = '.cecil';
45
46
    protected $subset = [];
47
48
    #[Inject]
49
    private TwigFactory $twigFactory;
50
51
    /**
52
     * {@inheritdoc}
53
     */
54 1
    public function getName(): string
55
    {
56 1
        return 'Rendering pages';
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62 1
    public function init(array $options): void
63
    {
64 1
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
65
            $message = \sprintf('"%s" is not a valid layouts directory', $this->config->getLayoutsPath());
66
            $this->logger->debug($message);
67
        }
68
69
        // render a subset of pages?
70 1
        if (!empty($options['render-subset'])) {
71
            $subset = \sprintf('pages.subsets.%s', (string) $options['render-subset']);
72
            if (!$this->config->has($subset)) {
73
                throw new ConfigException(\sprintf('Subset "%s" not found.', $subset));
74
            }
75
            $this->subset = (array) $this->config->get($subset);
76
        }
77
78 1
        $this->canProcess = true;
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     *
84
     * @throws RuntimeException
85
     */
86 1
    public function process(): void
87
    {
88
        // prepares renderer
89 1
        $this->builder->setRenderer($this->twigFactory->create($this->getAllLayoutsPaths()));
90
91
        // adds global variables
92 1
        $this->addGlobals();
93
94 1
        $subset = $this->subset;
95
96
        /** @var Collection $pages */
97 1
        $pages = $this->builder->getPages()
98 1
            // published only
99 1
            ->filter(function (Page $page) {
100 1
                return (bool) $page->getVariable('published');
101 1
            })
102 1
            ->filter(function (Page $page) use ($subset) {
103 1
                if (empty($subset)) {
104 1
                    return true;
105
                }
106
                if (
107
                    !empty($subset['path'])
108
                    && !((bool) preg_match('/' . (string) $subset['path'] . '/i', $page->getPath()))
109
                ) {
110
                    return false;
111
                }
112
                if (!empty($subset['language'])) {
113
                    $language = $page->getVariable('language', $this->config->getLanguageDefault());
114
                    if ($language !== (string) $subset['language']) {
115
                        return false;
116
                    }
117
                }
118
                return true;
119 1
            })
120 1
            // enrichs some variables
121 1
            ->map(function (Page $page) {
122 1
                $formats = $this->getOutputFormats($page);
123
                // output formats
124 1
                $page->setVariable('output', $formats);
125
                // alternates formats
126 1
                $page->setVariable('alternates', $this->getAlternates($formats));
127
                // translations
128 1
                $page->setVariable('translations', $this->getTranslations($page));
129
130 1
                return $page;
131 1
            });
132 1
        $total = \count($pages);
133
134
        // renders each page
135 1
        $count = 0;
136 1
        $postprocessors = [];
137 1
        foreach ((array) $this->config->get('output.postprocessors') as $name => $postprocessor) {
138
            try {
139 1
                if (!class_exists($postprocessor)) {
140 1
                    throw new RuntimeException(\sprintf('Class "%s" not found', $postprocessor));
141
                }
142 1
                $postprocessors[] = new $postprocessor($this->builder);
143 1
                $this->logger->debug(\sprintf('Output post processor "%s" loaded', $name));
144 1
            } catch (\Exception $e) {
145 1
                $this->logger->error(\sprintf('Unable to load output post processor "%s": %s', $name, $e->getMessage()));
146
            }
147
        }
148
149
        // some cache to avoid multiple calls
150 1
        $cache = [];
151
152
        /** @var Page $page */
153 1
        foreach ($pages as $page) {
154 1
            $count++;
155 1
            $rendered = [];
156
157
            // l10n
158 1
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
159 1
            if (!isset($cache['locale'][$language])) {
160 1
                $cache['locale'][$language] = $this->config->getLanguageProperty('locale', $language);
161
            }
162 1
            $this->builder->getRenderer()->setLocale($cache['locale'][$language]);
163
164
            // global site variables
165 1
            if (!isset($cache['site'][$language])) {
166 1
                $cache['site'][$language] = new Site($this->builder, $language);
167
            }
168 1
            $this->builder->getRenderer()->addGlobal('site', $cache['site'][$language]);
169
170
            // global config raw variables
171 1
            if (!isset($cache['config'][$language])) {
172 1
                $cache['config'][$language] = new RendererConfig($this->builder, $language);
173
            }
174 1
            $this->builder->getRenderer()->addGlobal('config', $cache['config'][$language]);
175
176
            // excluded format(s)?
177 1
            $formats = (array) $page->getVariable('output');
178 1
            foreach ($formats as $key => $format) {
179 1
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
180
                    // ie:
181
                    //   formats:
182
                    //     atom:
183
                    //       [...]
184
                    //       exclude: [paginated]
185 1
                    if (!\is_array($exclude)) {
186
                        $exclude = [$exclude];
187
                    }
188 1
                    foreach ($exclude as $variable) {
189 1
                        if ($page->hasVariable($variable)) {
190 1
                            unset($formats[$key]);
191
                        }
192
                    }
193
                }
194
            }
195
196
            // specific output format from subset
197 1
            if (!empty($this->subset['output'])) {
198
                $currentFormats = $formats;
199
                $formats = [];
200
                if (\in_array((string) $this->subset['output'], $currentFormats)) {
201
                    $formats = [(string) $this->subset['output']];
202
                }
203
            }
204
205
            // renders each output format
206 1
            foreach ($formats as $format) {
207
                // search for the template
208 1
                $layout = Layout::finder($page, $format, $this->config);
209
                // renders with Twig
210
                try {
211 1
                    $deprecations = [];
212 1
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
213 1
                        if (E_USER_DEPRECATED === $type) {
214 1
                            $deprecations[] = $msg;
215
                        }
216 1
                    });
217 1
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
218 1
                    foreach ($deprecations as $value) {
219 1
                        $this->builder->getLogger()->warning($value);
220
                    }
221 1
                    foreach ($postprocessors as $postprocessor) {
222 1
                        $output = $postprocessor->process($page, $output, $format);
223
                    }
224 1
                    $rendered[$format] = [
225 1
                        'output'   => $output,
226 1
                        'template' => [
227 1
                            'scope' => $layout['scope'],
228 1
                            'file'  => $layout['file'],
229 1
                        ],
230 1
                    ];
231 1
                    $page->addRendered($rendered);
232
                } catch (\Twig\Error\Error $e) {
233
                    throw new RuntimeException(
234
                        \sprintf(
235
                            'Unable to render template "%s" for page "%s".',
236
                            $e->getSourceContext()->getName(),
237
                            $page->getFileName() ?? $page->getId()
238
                        ),
239
                        previous: $e,
240
                        file: $e->getSourceContext()->getPath(),
241
                        line: $e->getTemplateLine(),
242
                    );
243
                } catch (\Exception $e) {
244
                    throw new RuntimeException($e->getMessage(), previous: $e);
245
                }
246
            }
247 1
            $this->builder->getPages()->replace($page->getId(), $page);
248
249 1
            $templates = array_column($rendered, 'template');
250 1
            $message = \sprintf(
251 1
                'Page "%s" rendered with [%s]',
252 1
                $page->getId() ?: 'index',
253 1
                Util\Str::combineArrayToString($templates, 'scope', 'file')
254 1
            );
255 1
            $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]);
256
        }
257
        // profiler
258 1
        if ($this->builder->isDebug()) {
259
            try {
260
                // HTML
261 1
                $htmlDumper = new \Twig\Profiler\Dumper\HtmlDumper();
262 1
                $profileHtmlFile = Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR, 'twig_profile.html');
263 1
                Util\File::getFS()->dumpFile($profileHtmlFile, $htmlDumper->dump($this->builder->getRenderer()->getDebugProfile()));
264
                // TXT
265 1
                $textDumper = new \Twig\Profiler\Dumper\TextDumper();
266 1
                $profileTextFile = Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR, 'twig_profile.txt');
267 1
                Util\File::getFS()->dumpFile($profileTextFile, $textDumper->dump($this->builder->getRenderer()->getDebugProfile()));
268
                // log
269 1
                $this->builder->getLogger()->debug(\sprintf('Twig profile dumped in "%s"', Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR)));
270
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
271
                throw new RuntimeException($e->getMessage());
272
            }
273
        }
274
    }
275
276
    /**
277
     * Returns an array of layouts directories.
278
     */
279 1
    protected function getAllLayoutsPaths(): array
280
    {
281 1
        $paths = [];
282
283
        // layouts/
284 1
        if (is_dir($this->config->getLayoutsPath())) {
285 1
            $paths[] = $this->config->getLayoutsPath();
286
        }
287
        // <theme>/layouts/
288 1
        if ($this->config->hasTheme()) {
289 1
            foreach ($this->config->getTheme() ?? [] as $theme) {
290 1
                $paths[] = $this->config->getThemeDirPath($theme);
291
            }
292
        }
293
        // resources/layouts/
294 1
        if (is_dir($this->config->getLayoutsInternalPath())) {
295 1
            $paths[] = $this->config->getLayoutsInternalPath();
296
        }
297
298 1
        return $paths;
299
    }
300
301
    /**
302
     * Adds global variables.
303
     */
304 1
    protected function addGlobals()
305
    {
306 1
        $this->builder->getRenderer()->addGlobal('cecil', [
307 1
            'url'       => \sprintf('https://cecil.app/#%s', Builder::getVersion()),
308 1
            'version'   => Builder::getVersion(),
309 1
            'poweredby' => \sprintf('Cecil v%s', Builder::getVersion()),
310 1
        ]);
311
    }
312
313
    /**
314
     * Get available output formats.
315
     *
316
     * @throws RuntimeException
317
     */
318 1
    protected function getOutputFormats(Page $page): array
319
    {
320
        // Get page output format(s) if defined.
321
        // ie:
322
        // ```yaml
323
        // output: txt
324
        // ```
325 1
        if ($page->getVariable('output')) {
326 1
            $formats = $page->getVariable('output');
327 1
            if (!\is_array($formats)) {
328 1
                $formats = [$formats];
329
            }
330
331 1
            return $formats;
332
        }
333
334
        // Get available output formats for the page type.
335
        // ie:
336
        // ```yaml
337
        // page: [html, json]
338
        // ```
339 1
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
340 1
        if (empty($formats)) {
341
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
342
        }
343 1
        if (!\is_array($formats)) {
344
            $formats = [$formats];
345
        }
346
347 1
        return array_unique($formats);
348
    }
349
350
    /**
351
     * Get alternates.
352
     */
353 1
    protected function getAlternates(array $formats): array
354
    {
355 1
        $alternates = [];
356
357 1
        if (\count($formats) > 1 || \in_array('html', $formats)) {
358 1
            foreach ($formats as $format) {
359 1
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
360 1
                $alternates[] = [
361 1
                    'rel'    => $rel,
362 1
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
363 1
                    'title'  => strtoupper($format),
364 1
                    'format' => $format,
365 1
                ];
366
            }
367
        }
368
369 1
        return $alternates;
370
    }
371
372
    /**
373
     * Returns the collection of translated pages for a given page.
374
     */
375 1
    protected function getTranslations(Page $refPage): Collection
376
    {
377 1
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
378 1
            return $page->getVariable('langref') == $refPage->getVariable('langref')
379 1
                && $page->getType() == $refPage->getType()
380 1
                && $page->getId() !== $refPage->getId()
381 1
                && !empty($page->getVariable('published'))
382 1
                && !$page->getVariable('paginated')
383 1
            ;
384 1
        });
385
386 1
        return $pages;
387
    }
388
}
389