Completed
Pull Request — master (#46)
by Vladimir
03:10
created

Compiler::setRedirectTemplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
        $this->theme = '';
62 14
    }
63
64
    /**
65
     * @param string|false $template
66
     */
67
    public function setRedirectTemplate($template)
68
    {
69
        $this->redirectTemplate = $template;
70
    }
71
72
    /**
73
     * @param Folder $folder
74
     */
75 14
    public function setTargetFolder(Folder $folder)
76
    {
77 14
        $this->folder = $folder;
78 14
    }
79
80
    /**
81
     * @param PageView[][] $pageViews
82
     * @param PageView[]   $pageViewsFlattened
83
     */
84 14
    public function setPageViews(array &$pageViews, array &$pageViewsFlattened)
85
    {
86 14
        $this->pageViews = &$pageViews;
87 14
        $this->pageViewsFlattened = &$pageViewsFlattened;
88 14
    }
89
90
    public function setThemeName($themeName)
91
    {
92
        $this->theme = $themeName;
93
    }
94
95
    ///
96
    // Twig parent templates
97
    ///
98
99
    public function isParentTemplate($filePath)
100
    {
101
        return isset($this->templateDependencies[$filePath]);
102
    }
103
104
    public function refreshParent($filePath)
105
    {
106
        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...
107
        {
108
            $this->compilePageView($parentTemplate);
109
        }
110
    }
111
112
    ///
113
    // IO Functionality
114
    ///
115
116
    /**
117
     * Compile all of the PageViews registered with the compiler.
118
     *
119
     * @since 0.1.0
120
     */
121 14
    public function compileAll()
122
    {
123 14
        foreach ($this->pageViewsFlattened as &$pageView)
124
        {
125 14
            $this->compilePageView($pageView);
126 14
        }
127 14
    }
128
129
    public function compileSome($filter = array())
130
    {
131
        /** @var PageView $pageView */
132
        foreach ($this->pageViewsFlattened as &$pageView)
133
        {
134
            if ($pageView->hasTwigDependency($filter['namespace'], $filter['dependency']))
135
            {
136
                $this->compilePageView($pageView);
137
            }
138
        }
139
    }
140
141
    /**
142
     * Compile an individual PageView item.
143
     *
144
     * This function will take care of determining *how* to treat the PageView and write the compiled output to a the
145
     * respective target file.
146
     *
147
     * @param DynamicPageView|RepeaterPageView|PageView $pageView The PageView that needs to be compiled
148
     *
149
     * @since 0.1.1
150
     */
151 14
    public function compilePageView(&$pageView)
152
    {
153 14
        $this->output->debug('Compiling {type} PageView: {pageview}', array(
154 14
            'pageview' => $pageView->getRelativeFilePath(),
155 14
            'type' => $pageView->getType()
156 14
        ));
157
158 14
        switch ($pageView->getType())
159
        {
160 14
            case PageView::STATIC_TYPE:
161 10
                $this->compileStaticPageView($pageView);
162 10
                $this->compileStandardRedirects($pageView);
163 10
                break;
164
165 4
            case PageView::DYNAMIC_TYPE:
166 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...
167 2
                $this->compileStandardRedirects($pageView);
168 2
                break;
169
170 2
            case PageView::REPEATER_TYPE:
171 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...
172 2
                $this->compileExpandedRedirects($pageView);
173 2
                break;
174 14
        }
175 14
    }
176
177
    /**
178
     * Write the compiled output for a static PageView.
179
     *
180
     * @param PageView $pageView
181
     *
182
     * @since 0.1.1
183
     */
184 10
    private function compileStaticPageView(&$pageView)
185
    {
186 10
        $targetFile = $pageView->getTargetFile();
187 10
        $output = $this->renderStaticPageView($pageView);
188
189 10
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
190 10
        $this->folder->writeFile($targetFile, $output);
191 10
    }
192
193
    /**
194
     * Write the compiled output for a dynamic PageView.
195
     *
196
     * @param DynamicPageView $pageView
197
     *
198
     * @since 0.1.1
199
     */
200 2
    private function compileDynamicPageViews(&$pageView)
201
    {
202 2
        $contentItems = $pageView->getContentItems();
203 2
        $template = $this->createTwigTemplate($pageView);
204
205 2
        foreach ($contentItems as &$contentItem)
206
        {
207 2
            if ($contentItem->isDraft() && !Service::getParameter(BuildableCommand::USE_DRAFTS))
208 2
            {
209 1
                $this->output->debug('{file}: marked as a draft', array(
210 1
                    'file' => $contentItem->getRelativeFilePath()
211 1
                ));
212
213 2
                continue;
214
            }
215
216 2
            $targetFile = $contentItem->getTargetFile();
217 2
            $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
218
219 2
            $this->output->notice('Writing file: {file}', array('file' => $targetFile));
220 2
            $this->folder->writeFile($targetFile, $output);
221 2
        }
222 2
    }
223
224
    /**
225
     * Write the compiled output for a repeater PageView.
226
     *
227
     * @param RepeaterPageView $pageView
228
     *
229
     * @since 0.1.1
230
     */
231 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...
232
    {
233 2
        $pageView->rewindPermalink();
234
235 2
        $template = $this->createTwigTemplate($pageView);
236 2
        $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...
237
238 2
        foreach ($permalinks as $permalink)
239
        {
240 2
            $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...
241 2
            $targetFile = $pageView->getTargetFile();
242 2
            $output = $this->renderRepeaterPageView($template, $pageView, $permalink);
243
244 2
            $this->output->notice('Writing repeater file: {file}', array('file' => $targetFile));
245 2
            $this->folder->writeFile($targetFile, $output);
246 2
        }
247 2
    }
248
249
    /**
250
     * @deprecated
251
     *
252
     * @todo This function needs to be rewritten or removed. Something
253
     *
254
     * @param ContentItem $contentItem
255
     */
256 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...
257
    {
258
        $pageView = $contentItem->getPageView();
259
        $template = $this->createTwigTemplate($pageView);
260
261
        $contentItem->evaluateFrontMatter($pageView->getFrontMatter(false));
262
263
        $targetFile = $contentItem->getTargetFile();
264
        $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
265
266
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
267
        $this->folder->writeFile($targetFile, $output);
268
    }
269
270
    ///
271
    // Redirect handling
272
    ///
273
274
    /**
275
     * Write redirects for standard redirects.
276
     *
277
     * @param PageView $pageView
278
     *
279
     * @since 0.1.1
280
     */
281 12
    private function compileStandardRedirects(&$pageView)
282
    {
283 12
        $redirects = $pageView->getRedirects();
284
285 12
        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...
286
        {
287 4
            $redirectPageView = PageView::createRedirect(
288 4
                $redirect,
289 4
                $pageView->getPermalink(),
290 4
                $this->redirectTemplate
291 4
            );
292
293 4
            $this->compileStaticPageView($redirectPageView);
294 12
        }
295 12
    }
296
297
    /**
298
     * Write redirects for expanded redirects.
299
     *
300
     * @param RepeaterPageView $pageView
301
     *
302
     * @since 0.1.1
303
     */
304 2
    private function compileExpandedRedirects(&$pageView)
305
    {
306 2
        $permalinks = $pageView->getRepeaterPermalinks();
307
308
        /** @var ExpandedValue[] $repeaterRedirect */
309 2
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
310
        {
311
            /**
312
             * @var int           $index
313
             * @var ExpandedValue $redirect
314
             */
315
            foreach ($repeaterRedirect as $index => $redirect)
316
            {
317
                $redirectPageView = PageView::createRedirect(
318
                    $redirect->getEvaluated(),
319
                    $permalinks[$index]->getEvaluated(),
320
                    $this->redirectTemplate
321
                );
322
                $this->compilePageView($redirectPageView);
323
            }
324 2
        }
325 2
    }
326
327
    ///
328
    // Twig Functionality
329
    ///
330
331
    /**
332
     * Get the compiled HTML for a specific iteration of a repeater PageView.
333
     *
334
     * @param Twig_Template $template
335
     * @param PageView      $pageView
336
     * @param ExpandedValue $expandedValue
337
     *
338
     * @since  0.1.1
339
     *
340
     * @return string
341
     */
342 2
    private function renderRepeaterPageView(&$template, &$pageView, &$expandedValue)
343
    {
344 2
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
345
346 2
        $pageView->setFrontMatter(array(
347 2
            'permalink' => $expandedValue->getEvaluated(),
348 2
            'iterators' => $expandedValue->getIterators(),
349 2
        ));
350
351
        return $template
352 2
            ->render(array(
353 2
                'this' => $pageView->createJail(),
354 2
            ));
355
    }
356
357
    /**
358
     * Get the compiled HTML for a specific ContentItem.
359
     *
360
     * @param Twig_Template $template
361
     * @param PageView      $pageView
362
     * @param ContentItem   $contentItem
363
     *
364
     * @since  0.1.1
365
     *
366
     * @return string
367
     */
368 2
    private function renderDynamicPageView(&$template, &$pageView, &$contentItem)
369
    {
370 2
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
371
372
        return $template
373 2
            ->render(array(
374 2
                'this' => $contentItem->createJail(),
375 2
            ));
376
    }
377
378
    /**
379
     * Get the compiled HTML for a static PageView.
380
     *
381
     * @param PageView $pageView
382
     *
383
     * @since  0.1.1
384
     *
385
     * @throws \Exception
386
     * @throws \Throwable
387
     * @throws Twig_Error_Syntax
388
     *
389
     * @return string
390
     */
391 10
    private function renderStaticPageView(&$pageView)
392
    {
393 10
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
394
395 10
        return $this
396 10
            ->createTwigTemplate($pageView)
397 10
            ->render(array(
398 10
                'this' => $pageView->createJail(),
399 10
            ));
400
    }
401
402
    /**
403
     * Create a Twig template that just needs an array to render.
404
     *
405
     * @param PageView $pageView The PageView whose body will be used for Twig compilation
406
     *
407
     * @since  0.1.1
408
     *
409
     * @throws \Exception
410
     * @throws \Throwable
411
     * @throws Twig_Error_Syntax
412
     *
413
     * @return Twig_Template
414
     */
415 14
    private function createTwigTemplate(&$pageView)
416
    {
417
        try
418
        {
419 14
            $template = $this->twig->createTemplate($pageView->getContent());
420
421 14
            if (Service::getParameter(BuildableCommand::WATCHING))
422 14
            {
423
                $parent = $template->getParent(array());
424
425
                while ($parent !== false)
426
                {
427
                    $path = str_replace('@theme', $this->fs->appendPath(ThemeManager::THEME_FOLDER, $this->theme), $parent->getTemplateName());
428
                    $this->templateDependencies[$path][] = &$pageView;
429
430
                    $parent = $parent->getParent(array());
431
                }
432
            }
433
434 14
            return $template;
435
        }
436
        catch (Twig_Error_Syntax $e)
437
        {
438
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
439
            $e->setSourceContext(new Twig_Source(
440
                $pageView->getContent(),
441
                $pageView->getName(),
442
                $pageView->getRelativeFilePath()
443
            ));
444
445
            throw $e;
446
        }
447
    }
448
}
449