Passed
Pull Request — master (#2172)
by Arnaud
04:54
created

Render   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Test Coverage

Coverage 81.61%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 159
c 2
b 0
f 0
dl 0
loc 334
ccs 142
cts 174
cp 0.8161
rs 5.5199
wmc 56

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A init() 0 15 5
A getAlternates() 0 17 5
A addGlobals() 0 6 1
B getOutputFormats() 0 35 7
A getAllLayoutsPaths() 0 20 5
A getTranslations() 0 11 5
F process() 0 176 27

How to fix   Complexity   

Complex Class

Complex classes like Render often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Render, and based on these observations, apply Extract Interface, too.

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