Passed
Pull Request — master (#2148)
by Arnaud
09:31 queued 04:12
created

Render::process()   F

Complexity

Conditions 19
Paths 5190

Size

Total Lines 145
Code Lines 85

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 77
CRAP Score 20.8369

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 19
eloc 85
c 2
b 0
f 0
nc 5190
nop 0
dl 0
loc 145
ccs 77
cts 93
cp 0.828
crap 20.8369
rs 0.3499

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