Passed
Pull Request — master (#1877)
by Arnaud
12:13 queued 05:29
created

Render   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Test Coverage

Coverage 88.65%

Importance

Changes 0
Metric Value
eloc 122
dl 0
loc 273
ccs 125
cts 141
cp 0.8865
rs 8.96
c 0
b 0
f 0
wmc 43

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A init() 0 8 3
F process() 0 131 18
A getAlternates() 0 17 5
A addGlobals() 0 6 1
A getOutputFormats() 0 30 5
A getAllLayoutsPaths() 0 21 5
A getTranslations() 0 11 5

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
    /**
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
        /** @var Collection $pages */
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
            // enrichs some variables
73 1
            ->map(function (Page $page) {
74 1
                $formats = $this->getOutputFormats($page);
75
                // output formats
76 1
                $page->setVariable('output', $formats);
77
                // alternates formats
78 1
                $page->setVariable('alternates', $this->getAlternates($formats));
79
                // translations
80 1
                $page->setVariable('translations', $this->getTranslations($page));
81
82 1
                return $page;
83 1
            });
84 1
        $total = \count($pages);
85
86
        // renders each page
87 1
        $count = 0;
88 1
        $postprocessors = [];
89 1
        foreach ($this->config->get('output.postprocessors') as $name => $postprocessor) {
90 1
            if (!class_exists($postprocessor)) {
91 1
                $this->builder->getLogger()->error(sprintf('Unable to load output post processor "%s"', $postprocessor));
92 1
                break;
93
            }
94 1
            $postprocessors[] = new $postprocessor($this->builder);
95 1
            $this->builder->getLogger()->debug(sprintf('Output post processor "%s" loaded', $name));
96
        }
97
        /** @var Page $page */
98 1
        foreach ($pages as $page) {
99 1
            $count++;
100 1
            $rendered = [];
101
102
            // l10n
103 1
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
104 1
            $locale = $this->config->getLanguageProperty('locale', $language);
105 1
            $this->builder->getRenderer()->setLocale($locale);
106
107
            // global site variables
108 1
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $language));
109
110
            // global config raw variables
111 1
            $this->builder->getRenderer()->addGlobal('config', new Config($this->builder, $language));
112
113
            // excluded format(s)?
114 1
            $formats = (array) $page->getVariable('output');
115 1
            foreach ($formats as $key => $format) {
116 1
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
117
                    // ie:
118
                    //   formats:
119
                    //     atom:
120
                    //       [...]
121
                    //       exclude: [paginated]
122 1
                    if (!\is_array($exclude)) {
123
                        $exclude = [$exclude];
124
                    }
125 1
                    foreach ($exclude as $variable) {
126 1
                        if ($page->hasVariable($variable)) {
127 1
                            unset($formats[$key]);
128
                        }
129
                    }
130
                }
131
            }
132
133
            // renders each output format
134 1
            foreach ($formats as $format) {
135
                // search for the template
136 1
                $layout = Layout::finder($page, $format, $this->config);
137
                // renders with Twig
138
                try {
139 1
                    $deprecations = [];
140 1
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
141 1
                        if (E_USER_DEPRECATED === $type) {
142
                            $deprecations[] = $msg;
143
                        }
144 1
                    });
145 1
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
146 1
                    foreach ($deprecations as $value) {
147
                        $this->builder->getLogger()->warning($value);
148
                    }
149 1
                    foreach ($postprocessors as $postprocessor) {
150 1
                        $output = $postprocessor->process($page, $output, $format);
151
                    }
152 1
                    $rendered[$format] = [
153 1
                        'output'   => $output,
154 1
                        'template' => [
155 1
                            'scope' => $layout['scope'],
156 1
                            'file'  => $layout['file'],
157 1
                        ],
158 1
                    ];
159 1
                    $page->addRendered($rendered);
160
                    // profiler
161 1
                    if ($this->builder->isDebug()) {
162 1
                        $dumper = new \Twig\Profiler\Dumper\HtmlDumper();
163 1
                        file_put_contents(
164 1
                            Util::joinFile($this->config->getOutputPath(), '_debug_twig_profile.html'),
165 1
                            $dumper->dump($this->builder->getRenderer()->getDebugProfile())
166 1
                        );
167
                    }
168
                } catch (\Twig\Error\Error $e) {
169
                    $template = !empty($e->getSourceContext()->getPath()) ? $e->getSourceContext()->getPath() : $e->getSourceContext()->getName();
170
171
                    throw new RuntimeException(sprintf(
172
                        'Template "%s%s" (page: %s): %s',
173
                        $template,
174
                        $e->getTemplateLine() >= 0 ? sprintf(':%s', $e->getTemplateLine()) : '',
175
                        $page->getId(),
176
                        $e->getMessage()
177
                    ));
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
    }
191
192
    /**
193
     * Returns an array of layouts directories.
194
     */
195 1
    protected function getAllLayoutsPaths(): array
196
    {
197 1
        $paths = [];
198
199
        // layouts/
200 1
        if (is_dir($this->config->getLayoutsPath())) {
201 1
            $paths[] = $this->config->getLayoutsPath();
202
        }
203
        // <theme>/layouts/
204 1
        if ($this->config->hasTheme()) {
205 1
            $themes = $this->config->getTheme();
206 1
            foreach ($themes as $theme) {
207 1
                $paths[] = $this->config->getThemeDirPath($theme);
208
            }
209
        }
210
        // resources/layouts/
211 1
        if (is_dir($this->config->getLayoutsInternalPath())) {
212 1
            $paths[] = $this->config->getLayoutsInternalPath();
213
        }
214
215 1
        return $paths;
216
    }
217
218
    /**
219
     * Adds global variables.
220
     */
221 1
    protected function addGlobals()
222
    {
223 1
        $this->builder->getRenderer()->addGlobal('cecil', [
224 1
            'url'       => sprintf('https://cecil.app/#%s', Builder::getVersion()),
225 1
            'version'   => Builder::getVersion(),
226 1
            'poweredby' => sprintf('Cecil v%s', Builder::getVersion()),
227 1
        ]);
228
    }
229
230
    /**
231
     * Get available output formats.
232
     *
233
     * @throws RuntimeException
234
     */
235 1
    protected function getOutputFormats(Page $page): array
236
    {
237
        // Get page output format(s) if defined.
238
        // ie:
239
        // ```yaml
240
        // output: txt
241
        // ```
242 1
        if ($page->getVariable('output')) {
243 1
            $formats = $page->getVariable('output');
244 1
            if (!\is_array($formats)) {
245 1
                $formats = [$formats];
246
            }
247
248 1
            return $formats;
249
        }
250
251
        // Get available output formats for the page type.
252
        // ie:
253
        // ```yaml
254
        // page: [html, json]
255
        // ```
256 1
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
257 1
        if (empty($formats)) {
258
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
259
        }
260 1
        if (!\is_array($formats)) {
261
            $formats = [$formats];
262
        }
263
264 1
        return $formats;
265
    }
266
267
    /**
268
     * Get alternates.
269
     */
270 1
    protected function getAlternates(array $formats): array
271
    {
272 1
        $alternates = [];
273
274 1
        if (\count($formats) > 1 || \in_array('html', $formats)) {
275 1
            foreach ($formats as $format) {
276 1
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
277 1
                $alternates[] = [
278 1
                    'rel'    => $rel,
279 1
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
280 1
                    'title'  => strtoupper($format),
281 1
                    'format' => $format,
282 1
                ];
283
            }
284
        }
285
286 1
        return $alternates;
287
    }
288
289
    /**
290
     * Returns the collection of translated pages for a given page.
291
     */
292 1
    protected function getTranslations(Page $refPage): \Cecil\Collection\Page\Collection
293
    {
294 1
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
295 1
            return $page->getId() !== $refPage->getId()
296 1
                && $page->getVariable('langref') == $refPage->getVariable('langref')
297 1
                && $page->getType() == $refPage->getType()
298 1
                && !empty($page->getVariable('published'))
299 1
                && !$page->getVariable('paginated');
300 1
        });
301
302 1
        return $pages;
303
    }
304
}
305