Completed
Pull Request — master (#46)
by Vladimir
02:32
created

Compiler::compilePageView()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.128

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 16
cts 20
cp 0.8
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 4
nop 1
crap 4.128
1
<?php
2
3
/**
4
 * @copyright 2017 Vladimir Jimenez
5
 * @license   https://github.com/allejo/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx;
9
10
use allejo\stakx\Command\BuildableCommand;
11
use allejo\stakx\Document\ContentItem;
12
use allejo\stakx\Document\DynamicPageView;
13
use allejo\stakx\Document\PageView;
14
use allejo\stakx\Document\RepeaterPageView;
15
use allejo\stakx\FrontMatter\ExpandedValue;
16
use allejo\stakx\Manager\BaseManager;
17
use allejo\stakx\Manager\ThemeManager;
18
use allejo\stakx\Manager\TwigManager;
19
use allejo\stakx\System\Folder;
20
use Twig_Environment;
21
use Twig_Error_Syntax;
22
use Twig_Source;
23
use Twig_Template;
24
25
/**
26
 * This class takes care of rendering the Twig body of PageViews with the respective information and it also takes care
27
 * of writing the rendered Twig to the filesystem.
28
 *
29
 * @internal
30
 *
31
 * @since 0.1.1
32
 */
33
class Compiler extends BaseManager
34
{
35
    /** @var string|false */
36
    private $redirectTemplate;
37
38
    /** @var Twig_Template[] */
39
    private $templateDependencies;
40
41
    /** @var PageView[] */
42
    private $pageViewsFlattened;
43
44
    /** @var PageView[][] */
45
    private $pageViews;
46
47
    /** @var Folder */
48
    private $folder;
49
50
    /** @var string */
51
    private $theme;
52
53
    /** @var Twig_Environment */
54
    private $twig;
55
56 14
    public function __construct()
57
    {
58 14
        parent::__construct();
59
60 14
        $this->twig = TwigManager::getInstance();
61 14
    }
62
63
    /**
64
     * @param string|false $template
65
     */
66
    public function setRedirectTemplate($template)
67
    {
68
        $this->redirectTemplate = $template;
69
    }
70
71
    /**
72
     * @param Folder $folder
73
     */
74 14
    public function setTargetFolder(Folder $folder)
75
    {
76 14
        $this->folder = $folder;
77 14
    }
78
79
    /**
80
     * @param PageView[][] $pageViews
81
     * @param PageView[]   $pageViewsFlattened
82
     */
83 14
    public function setPageViews(array &$pageViews, array &$pageViewsFlattened)
84
    {
85 14
        $this->pageViews = &$pageViews;
86 14
        $this->pageViewsFlattened = &$pageViewsFlattened;
87 14
    }
88
89
    public function setThemeName($themeName)
90
    {
91
        $this->theme = $themeName;
92
    }
93
94
    ///
95
    // Twig parent templates
96
    ///
97
98
    public function isParentTemplate($filePath)
99
    {
100
        return isset($this->templateDependencies[$filePath]);
101
    }
102
103
    public function refreshParent($filePath)
104
    {
105
        foreach ($this->templateDependencies[$filePath] as &$parentTemplate)
0 ignored issues
show
Bug introduced by
The expression $this->templateDependencies[$filePath] of type object<Twig_Template> is not traversable.
Loading history...
106
        {
107
            $this->compilePageView($parentTemplate);
108
        }
109
    }
110
111
    ///
112
    // IO Functionality
113
    ///
114
115
    /**
116
     * Compile all of the PageViews registered with the compiler.
117
     *
118
     * @since 0.1.0
119
     */
120 14
    public function compileAll()
121
    {
122 14
        foreach ($this->pageViewsFlattened as &$pageView)
123 1
        {
124 14
            $this->compilePageView($pageView);
125 1
        }
126 1
    }
127
128
    public function compileSome($filter = array())
129
    {
130
        /** @var PageView $pageView */
131
        foreach ($this->pageViewsFlattened as &$pageView)
132
        {
133
            if ($pageView->hasTwigDependency($filter['namespace'], $filter['dependency']))
134
            {
135
                $this->compilePageView($pageView);
136
            }
137
        }
138
    }
139
140
    /**
141
     * Compile an individual PageView item.
142
     *
143
     * This function will take care of determining *how* to treat the PageView and write the compiled output to a the
144
     * respective target file.
145
     *
146
     * @param DynamicPageView|RepeaterPageView|PageView $pageView The PageView that needs to be compiled
147
     *
148
     * @since 0.1.1
149
     */
150 14
    public function compilePageView(&$pageView)
151
    {
152 14
        $this->output->debug('Compiling {type} PageView: {pageview}', array(
153 14
            'pageview' => $pageView->getRelativeFilePath(),
154 14
            'type' => $pageView->getType()
155 14
        ));
156
157 14
        switch ($pageView->getType())
158
        {
159 14
            case PageView::STATIC_TYPE:
160 10
                $this->compileStaticPageView($pageView);
161 1
                $this->compileStandardRedirects($pageView);
162 1
                break;
163
164 4
            case PageView::DYNAMIC_TYPE:
165 2
                $this->compileDynamicPageViews($pageView);
1 ignored issue
show
Compatibility introduced by
$pageView of type object<allejo\stakx\Document\PageView> is not a sub-type of object<allejo\stakx\Document\DynamicPageView>. It seems like you assume a child class of the class allejo\stakx\Document\PageView to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
166
                $this->compileStandardRedirects($pageView);
167
                break;
168
169 2
            case PageView::REPEATER_TYPE:
170 2
                $this->compileRepeaterPageViews($pageView);
1 ignored issue
show
Compatibility introduced by
$pageView of type object<allejo\stakx\Document\PageView> is not a sub-type of object<allejo\stakx\Document\RepeaterPageView>. It seems like you assume a child class of the class allejo\stakx\Document\PageView to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
171
                $this->compileExpandedRedirects($pageView);
172
                break;
173 1
        }
174 1
    }
175
176
    /**
177
     * Write the compiled output for a static PageView.
178
     *
179
     * @param PageView $pageView
180
     *
181
     * @since 0.1.1
182
     */
183 10
    private function compileStaticPageView(&$pageView)
184
    {
185 10
        $targetFile = $pageView->getTargetFile();
186 10
        $output = $this->renderStaticPageView($pageView);
187
188 1
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
189 1
        $this->folder->writeFile($targetFile, $output);
190 1
    }
191
192
    /**
193
     * Write the compiled output for a dynamic PageView.
194
     *
195
     * @param DynamicPageView $pageView
196
     *
197
     * @since 0.1.1
198
     */
199 2
    private function compileDynamicPageViews(&$pageView)
200
    {
201 2
        $contentItems = $pageView->getContentItems();
202 2
        $template = $this->createTwigTemplate($pageView);
203
204
        foreach ($contentItems as &$contentItem)
205
        {
206
            if ($contentItem->isDraft() && !Service::getParameter(BuildableCommand::USE_DRAFTS))
207
            {
208
                $this->output->debug('{file}: marked as a draft', array(
209
                    'file' => $contentItem->getRelativeFilePath()
210
                ));
211
212
                continue;
213 1
            }
214
215
            $targetFile = $contentItem->getTargetFile();
216
            $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
217
218
            $this->output->notice('Writing file: {file}', array('file' => $targetFile));
219
            $this->folder->writeFile($targetFile, $output);
220
        }
221
    }
222
223
    /**
224
     * Write the compiled output for a repeater PageView.
225
     *
226
     * @param RepeaterPageView $pageView
227
     *
228
     * @since 0.1.1
229
     */
230 2 View Code Duplication
    private function compileRepeaterPageViews(&$pageView)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
    {
232 2
        $pageView->rewindPermalink();
233
234 2
        $template = $this->createTwigTemplate($pageView);
235
        $permalinks = $pageView->getRepeaterPermalinks();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class allejo\stakx\Document\PageView as the method getRepeaterPermalinks() does only exist in the following sub-classes of allejo\stakx\Document\PageView: allejo\stakx\Document\RepeaterPageView. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
236
237
        foreach ($permalinks as $permalink)
238
        {
239
            $pageView->bumpPermalink();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class allejo\stakx\Document\PageView as the method bumpPermalink() does only exist in the following sub-classes of allejo\stakx\Document\PageView: allejo\stakx\Document\RepeaterPageView. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
240
            $targetFile = $pageView->getTargetFile();
241
            $output = $this->renderRepeaterPageView($template, $pageView, $permalink);
242
243
            $this->output->notice('Writing repeater file: {file}', array('file' => $targetFile));
244
            $this->folder->writeFile($targetFile, $output);
245
        }
246
    }
247
248
    /**
249
     * @deprecated
250
     *
251
     * @todo This function needs to be rewritten or removed. Something
252
     *
253
     * @param ContentItem $contentItem
254
     */
255 View Code Duplication
    public function compileContentItem(&$contentItem)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
256
    {
257
        $pageView = $contentItem->getPageView();
258
        $template = $this->createTwigTemplate($pageView);
259
260
        $contentItem->evaluateFrontMatter($pageView->getFrontMatter(false));
261
262
        $targetFile = $contentItem->getTargetFile();
263
        $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
264
265
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
266
        $this->folder->writeFile($targetFile, $output);
267
    }
268
269
    ///
270
    // Redirect handling
271
    ///
272
273
    /**
274
     * Write redirects for standard redirects.
275
     *
276
     * @param PageView $pageView
277
     *
278
     * @since 0.1.1
279
     */
280 1
    private function compileStandardRedirects(&$pageView)
281
    {
282 1
        $redirects = $pageView->getRedirects();
283
284 1
        foreach ($redirects as $redirect)
0 ignored issues
show
Bug introduced by
The expression $redirects of type null|array<integer,string> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
285
        {
286 1
            $redirectPageView = PageView::createRedirect(
287 1
                $redirect,
288 1
                $pageView->getPermalink(),
289 1
                $this->redirectTemplate
290 1
            );
291
292 1
            $this->compileStaticPageView($redirectPageView);
293 1
        }
294 1
    }
295
296
    /**
297
     * Write redirects for expanded redirects.
298
     *
299
     * @param RepeaterPageView $pageView
300
     *
301
     * @since 0.1.1
302
     */
303
    private function compileExpandedRedirects(&$pageView)
304
    {
305
        $permalinks = $pageView->getRepeaterPermalinks();
306
307
        /** @var ExpandedValue[] $repeaterRedirect */
308
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
309
        {
310
            /**
311
             * @var int           $index
312
             * @var ExpandedValue $redirect
313
             */
314
            foreach ($repeaterRedirect as $index => $redirect)
315
            {
316
                $redirectPageView = PageView::createRedirect(
317
                    $redirect->getEvaluated(),
318
                    $permalinks[$index]->getEvaluated(),
319
                    $this->redirectTemplate
320
                );
321
                $this->compilePageView($redirectPageView);
322
            }
323
        }
324
    }
325
326
    ///
327
    // Twig Functionality
328
    ///
329
330
    /**
331
     * Get the compiled HTML for a specific iteration of a repeater PageView.
332
     *
333
     * @param Twig_Template $template
334
     * @param PageView      $pageView
335
     * @param ExpandedValue $expandedValue
336
     *
337
     * @since  0.1.1
338
     *
339
     * @return string
340
     */
341
    private function renderRepeaterPageView(&$template, &$pageView, &$expandedValue)
342
    {
343
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
344
345
        $pageView->setFrontMatter(array(
346
            'permalink' => $expandedValue->getEvaluated(),
347
            'iterators' => $expandedValue->getIterators(),
348
        ));
349
350
        return $template
351
            ->render(array(
352
                'this' => $pageView->createJail(),
353
            ));
354
    }
355
356
    /**
357
     * Get the compiled HTML for a specific ContentItem.
358
     *
359
     * @param Twig_Template $template
360
     * @param PageView      $pageView
361
     * @param ContentItem   $contentItem
362
     *
363
     * @since  0.1.1
364
     *
365
     * @return string
366
     */
367
    private function renderDynamicPageView(&$template, &$pageView, &$contentItem)
368
    {
369
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
370
371
        return $template
372
            ->render(array(
373
                'this' => $contentItem->createJail(),
374
            ));
375
    }
376
377
    /**
378
     * Get the compiled HTML for a static PageView.
379
     *
380
     * @param PageView $pageView
381
     *
382
     * @since  0.1.1
383
     *
384
     * @throws \Exception
385
     * @throws \Throwable
386
     * @throws Twig_Error_Syntax
387
     *
388
     * @return string
389
     */
390 10
    private function renderStaticPageView(&$pageView)
391
    {
392 10
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
393
394 10
        return $this
395 10
            ->createTwigTemplate($pageView)
396 1
            ->render(array(
397 1
                'this' => $pageView->createJail(),
398 1
            ));
399
    }
400
401
    /**
402
     * Create a Twig template that just needs an array to render.
403
     *
404
     * @param PageView $pageView The PageView whose body will be used for Twig compilation
405
     *
406
     * @since  0.1.1
407
     *
408
     * @throws \Exception
409
     * @throws \Throwable
410
     * @throws Twig_Error_Syntax
411
     *
412
     * @return Twig_Template
413
     */
414 14
    private function createTwigTemplate(&$pageView)
415
    {
416
        try
417
        {
418 14
            $template = $this->twig->createTemplate($pageView->getContent());
419
420 14
            if (Service::getParameter(BuildableCommand::WATCHING))
421 1
            {
422
                $parent = $template->getParent(array());
423
424
                while ($parent !== false)
425
                {
426
                    $path = str_replace('@theme', $this->fs->appendPath(ThemeManager::THEME_FOLDER, $this->theme), $parent->getTemplateName());
427
                    $this->templateDependencies[$path][] = &$pageView;
428
429
                    $parent = $parent->getParent(array());
430
                }
431
            }
432
433 1
            return $template;
434
        }
435 13
        catch (Twig_Error_Syntax $e)
436
        {
437
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
438
            $e->setSourceContext(new Twig_Source(
439
                $pageView->getContent(),
440
                $pageView->getName(),
441
                $pageView->getRelativeFilePath()
442
            ));
443
444
            throw $e;
445
        }
446
    }
447
}
448