Passed
Push — nested-sections ( 37f302...6ff6f7 )
by Arnaud
04:43
created

Render::process()   F

Complexity

Conditions 19
Paths 1515

Size

Total Lines 135
Code Lines 76

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 77
CRAP Score 19.4143

Importance

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