Completed
Pull Request — master (#45)
by Vladimir
03:06
created

Compiler::compilePageView()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 18
cts 18
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 4
nop 1
crap 4
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\Document\ContentItem;
11
use allejo\stakx\Document\DynamicPageView;
12
use allejo\stakx\Document\PageView;
13
use allejo\stakx\Document\RepeaterPageView;
14
use allejo\stakx\FrontMatter\ExpandedValue;
15
use allejo\stakx\Manager\BaseManager;
16
use allejo\stakx\Manager\TwigManager;
17
use allejo\stakx\System\Folder;
18
use Twig_Environment;
19
use Twig_Error_Syntax;
20
use Twig_Source;
21
use Twig_Template;
22
23
/**
24
 * This class takes care of rendering the Twig body of PageViews with the respective information and it also takes care
25
 * of writing the rendered Twig to the filesystem.
26
 *
27
 * @internal
28
 *
29
 * @since 0.1.1
30
 */
31
class Compiler extends BaseManager
32
{
33
    /** @var string|false */
34
    private $redirectTemplate;
35
36
    /** @var PageView[] */
37
    private $pageViewsFlattened;
38
39
    /** @var PageView[][] */
40
    private $pageViews;
41
42
    /** @var Folder */
43
    private $folder;
44
45
    /** @var Twig_Environment */
46
    private $twig;
47
48 13
    public function __construct()
49
    {
50 13
        parent::__construct();
51
52 13
        $this->twig = TwigManager::getInstance();
53 13
    }
54
55
    /**
56
     * @param string|false $template
57
     */
58
    public function setRedirectTemplate($template)
59
    {
60
        $this->redirectTemplate = $template;
61
    }
62
63
    /**
64
     * @param Folder $folder
65
     */
66 13
    public function setTargetFolder(Folder $folder)
67
    {
68 13
        $this->folder = $folder;
69 13
    }
70
71
    /**
72
     * @param PageView[][] $pageViews
73
     * @param PageView[]   $pageViewsFlattened
74
     */
75 13
    public function setPageViews(array &$pageViews, array &$pageViewsFlattened)
76
    {
77 13
        $this->pageViews = &$pageViews;
78 13
        $this->pageViewsFlattened = &$pageViewsFlattened;
79 13
    }
80
81
    ///
82
    // IO Functionality
83
    ///
84
85
    /**
86
     * Compile all of the PageViews registered with the compiler.
87
     *
88
     * @since 0.1.0
89
     */
90 13
    public function compileAll()
91
    {
92 13
        foreach ($this->pageViewsFlattened as &$pageView)
93
        {
94 13
            $this->compilePageView($pageView);
95
        }
96 13
    }
97
98
    public function compileSome($filter = array())
99
    {
100
        /** @var PageView $pageView */
101
        foreach ($this->pageViewsFlattened as &$pageView)
102
        {
103
            if ($pageView->hasTwigDependency($filter['namespace'], $filter['dependency']))
104
            {
105
                $this->compilePageView($pageView);
106
            }
107
        }
108
    }
109
110
    /**
111
     * Compile an individual PageView item.
112
     *
113
     * This function will take care of determining *how* to treat the PageView and write the compiled output to a the
114
     * respective target file.
115
     *
116
     * @param DynamicPageView|RepeaterPageView|PageView $pageView The PageView that needs to be compiled
117
     *
118
     * @since 0.1.1
119
     */
120 13
    private function compilePageView(&$pageView)
121
    {
122 13
        $this->output->debug('Compiling {type} PageView: {pageview}', array(
123 13
            'pageview' => $pageView->getRelativeFilePath(),
124 13
            'type' => $pageView->getType()
125
        ));
126
127 13
        switch ($pageView->getType())
128
        {
129 13
            case PageView::STATIC_TYPE:
130 10
                $this->compileStaticPageView($pageView);
131 10
                $this->compileStandardRedirects($pageView);
132 10
                break;
133
134 3
            case PageView::DYNAMIC_TYPE:
135 1
                $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...
136 1
                $this->compileStandardRedirects($pageView);
137 1
                break;
138
139 2
            case PageView::REPEATER_TYPE:
140 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...
141 2
                $this->compileExpandedRedirects($pageView);
142 2
                break;
143
        }
144 13
    }
145
146
    /**
147
     * Write the compiled output for a static PageView.
148
     *
149
     * @param PageView $pageView
150
     *
151
     * @since 0.1.1
152
     */
153 10
    private function compileStaticPageView(&$pageView)
154
    {
155 10
        $targetFile = $pageView->getTargetFile();
156 10
        $output = $this->renderStaticPageView($pageView);
157
158 10
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
159 10
        $this->folder->writeFile($targetFile, $output);
160 10
    }
161
162
    /**
163
     * Write the compiled output for a dynamic PageView.
164
     *
165
     * @param DynamicPageView $pageView
166
     *
167
     * @since 0.1.1
168
     */
169 1
    private function compileDynamicPageViews(&$pageView)
170
    {
171 1
        $contentItems = $pageView->getContentItems();
172 1
        $template = $this->createTwigTemplate($pageView);
173
174 1
        foreach ($contentItems as &$contentItem)
175
        {
176 1
            if (!$contentItem->hasOutput())
177
            {
178
                $this->output->debug('{file}: marked for no output', array(
179
                    'file' => $contentItem->getRelativeFilePath()
180
                ));
181
182
                continue;
183
            }
184
185 1
            $targetFile = $contentItem->getTargetFile();
186 1
            $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
187
188 1
            $this->output->notice('Writing file: {file}', array('file' => $targetFile));
189 1
            $this->folder->writeFile($targetFile, $output);
190
        }
191 1
    }
192
193
    /**
194
     * Write the compiled output for a repeater PageView.
195
     *
196
     * @param RepeaterPageView $pageView
197
     *
198
     * @since 0.1.1
199
     */
200 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...
201
    {
202 2
        $pageView->rewindPermalink();
203
204 2
        $template = $this->createTwigTemplate($pageView);
205 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...
206
207 2
        foreach ($permalinks as $permalink)
208
        {
209 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...
210 2
            $targetFile = $pageView->getTargetFile();
211 2
            $output = $this->renderRepeaterPageView($template, $pageView, $permalink);
212
213 2
            $this->output->notice('Writing repeater file: {file}', array('file' => $targetFile));
214 2
            $this->folder->writeFile($targetFile, $output);
215
        }
216 2
    }
217
218
    /**
219
     * @deprecated
220
     *
221
     * @todo This function needs to be rewritten or removed. Something
222
     *
223
     * @param ContentItem $contentItem
224
     */
225 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...
226
    {
227
        $pageView = $contentItem->getPageView();
228
        $template = $this->createTwigTemplate($pageView);
229
230
        $contentItem->evaluateFrontMatter($pageView->getFrontMatter(false));
231
232
        $targetFile = $contentItem->getTargetFile();
233
        $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
234
235
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
236
        $this->folder->writeFile($targetFile, $output);
237
    }
238
239
    ///
240
    // Redirect handling
241
    ///
242
243
    /**
244
     * Write redirects for standard redirects.
245
     *
246
     * @param PageView $pageView
247
     *
248
     * @since 0.1.1
249
     */
250 11
    private function compileStandardRedirects(&$pageView)
251
    {
252 11
        $redirects = $pageView->getRedirects();
253
254 11
        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...
255
        {
256 4
            $redirectPageView = PageView::createRedirect(
257
                $redirect,
258 4
                $pageView->getPermalink(),
259 4
                $this->redirectTemplate
260
            );
261
262 4
            $this->compileStaticPageView($redirectPageView);
263
        }
264 11
    }
265
266
    /**
267
     * Write redirects for expanded redirects.
268
     *
269
     * @param RepeaterPageView $pageView
270
     *
271
     * @since 0.1.1
272
     */
273 2
    private function compileExpandedRedirects(&$pageView)
274
    {
275 2
        $permalinks = $pageView->getRepeaterPermalinks();
276
277
        /** @var ExpandedValue[] $repeaterRedirect */
278 2
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
279
        {
280
            /**
281
             * @var int           $index
282
             * @var ExpandedValue $redirect
283
             */
284
            foreach ($repeaterRedirect as $index => $redirect)
285
            {
286
                $redirectPageView = PageView::createRedirect(
287
                    $redirect->getEvaluated(),
288
                    $permalinks[$index]->getEvaluated(),
289
                    $this->redirectTemplate
290
                );
291
                $this->compilePageView($redirectPageView);
292
            }
293
        }
294 2
    }
295
296
    ///
297
    // Twig Functionality
298
    ///
299
300
    /**
301
     * Get the compiled HTML for a specific iteration of a repeater PageView.
302
     *
303
     * @param Twig_Template $template
304
     * @param PageView      $pageView
305
     * @param ExpandedValue $expandedValue
306
     *
307
     * @since  0.1.1
308
     *
309
     * @return string
310
     */
311 2
    private function renderRepeaterPageView(&$template, &$pageView, &$expandedValue)
312
    {
313 2
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
314
315 2
        $pageView->setFrontMatter(array(
316 2
            'permalink' => $expandedValue->getEvaluated(),
317 2
            'iterators' => $expandedValue->getIterators(),
318
        ));
319
320
        return $template
321 2
            ->render(array(
322 2
                'this' => $pageView->createJail(),
323
            ));
324
    }
325
326
    /**
327
     * Get the compiled HTML for a specific ContentItem.
328
     *
329
     * @param Twig_Template $template
330
     * @param PageView      $pageView
331
     * @param ContentItem   $contentItem
332
     *
333
     * @since  0.1.1
334
     *
335
     * @return string
336
     */
337 1
    private function renderDynamicPageView(&$template, &$pageView, &$contentItem)
338
    {
339 1
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
340
341
        return $template
342 1
            ->render(array(
343 1
                'this' => $contentItem->createJail(),
344
            ));
345
    }
346
347
    /**
348
     * Get the compiled HTML for a static PageView.
349
     *
350
     * @param PageView $pageView
351
     *
352
     * @since  0.1.1
353
     *
354
     * @throws \Exception
355
     * @throws \Throwable
356
     * @throws Twig_Error_Syntax
357
     *
358
     * @return string
359
     */
360 10
    private function renderStaticPageView(&$pageView)
361
    {
362 10
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
363
364
        return $this
365 10
            ->createTwigTemplate($pageView)
366 10
            ->render(array(
367 10
                'this' => $pageView->createJail(),
368
            ));
369
    }
370
371
    /**
372
     * Create a Twig template that just needs an array to render.
373
     *
374
     * @param PageView $pageView The PageView whose body will be used for Twig compilation
375
     *
376
     * @since  0.1.1
377
     *
378
     * @throws \Exception
379
     * @throws \Throwable
380
     * @throws Twig_Error_Syntax
381
     *
382
     * @return Twig_Template
383
     */
384 13
    private function createTwigTemplate(&$pageView)
385
    {
386
        try
387
        {
388 13
            return $this->twig->createTemplate($pageView->getContent());
389
        }
390
        catch (Twig_Error_Syntax $e)
391
        {
392
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
393
            $e->setSourceContext(new Twig_Source(
394
                $pageView->getContent(),
395
                $pageView->getName(),
396
                $pageView->getRelativeFilePath()
397
            ));
398
399
            throw $e;
400
        }
401
    }
402
}
403