Render::process()   F
last analyzed

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 186
Code Lines 109

Duplication

Lines 0
Ratio 0 %

Importance

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