Completed
Pull Request — master (#42)
by Vladimir
03:07
created

Compiler::compilePageView()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 1
dl 0
loc 20
ccs 17
cts 17
cp 1
crap 4
rs 9.2
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\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 13
        }
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
        switch ($pageView->getType())
123 1
        {
124 13
            case PageView::STATIC_TYPE:
125 10
                $this->compileStaticPageView($pageView);
126 10
                $this->compileStandardRedirects($pageView);
127 10
                break;
128
129 3
            case PageView::DYNAMIC_TYPE:
130 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...
131 1
                $this->compileStandardRedirects($pageView);
132 1
                break;
133
134 2
            case PageView::REPEATER_TYPE:
135 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...
136 2
                $this->compileExpandedRedirects($pageView);
137 2
                break;
138 13
        }
139 13
    }
140
141
    /**
142
     * Write the compiled output for a static PageView.
143
     *
144
     * @param PageView $pageView
145
     *
146
     * @since 0.1.1
147
     */
148 10
    private function compileStaticPageView(&$pageView)
149
    {
150 10
        $targetFile = $pageView->getTargetFile();
151 10
        $output = $this->renderStaticPageView($pageView);
152
153 10
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
154 10
        $this->folder->writeFile($targetFile, $output);
155 10
    }
156
157
    /**
158
     * Write the compiled output for a dynamic PageView.
159
     *
160
     * @param DynamicPageView $pageView
161
     *
162
     * @since 0.1.1
163
     */
164 1 View Code Duplication
    private function compileDynamicPageViews(&$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...
165
    {
166 1
        $contentItems = $pageView->getContentItems();
167 1
        $template = $this->createTwigTemplate($pageView);
168
169 1
        foreach ($contentItems as &$contentItem)
170
        {
171 1
            $targetFile = $contentItem->getTargetFile();
172 1
            $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
173
174 1
            $this->output->notice('Writing file: {file}', array('file' => $targetFile));
175 1
            $this->folder->writeFile($targetFile, $output);
176 1
        }
177 1
    }
178
179
    /**
180
     * Write the compiled output for a repeater PageView.
181
     *
182
     * @param RepeaterPageView $pageView
183
     *
184
     * @since 0.1.1
185
     */
186 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...
187
    {
188 2
        $pageView->rewindPermalink();
189
190 2
        $template = $this->createTwigTemplate($pageView);
191 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...
192
193 2
        foreach ($permalinks as $permalink)
194
        {
195 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...
196 2
            $targetFile = $pageView->getTargetFile();
197 2
            $output = $this->renderRepeaterPageView($template, $pageView, $permalink);
198
199 2
            $this->output->notice('Writing repeater file: {file}', array('file' => $targetFile));
200 2
            $this->folder->writeFile($targetFile, $output);
201 2
        }
202 2
    }
203
204
    /**
205
     * @deprecated
206
     *
207
     * @todo This function needs to be rewritten or removed. Something
208
     *
209
     * @param ContentItem $contentItem
210
     */
211 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...
212
    {
213
        $pageView = $contentItem->getPageView();
214
        $template = $this->createTwigTemplate($pageView);
215
216
        $contentItem->evaluateFrontMatter($pageView->getFrontMatter(false));
217
218
        $targetFile = $contentItem->getTargetFile();
219
        $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
220
221
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
222
        $this->folder->writeFile($targetFile, $output);
223
    }
224
225
    ///
226
    // Redirect handling
227
    ///
228
229
    /**
230
     * Write redirects for standard redirects.
231
     *
232
     * @param PageView $pageView
233
     *
234
     * @since 0.1.1
235
     */
236 11
    private function compileStandardRedirects(&$pageView)
237
    {
238 11
        $redirects = $pageView->getRedirects();
239
240 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...
241
        {
242 4
            $redirectPageView = PageView::createRedirect(
243 4
                $redirect,
244 4
                $pageView->getPermalink(),
245 4
                $this->redirectTemplate
246 4
            );
247
248 4
            $this->compileStaticPageView($redirectPageView);
249 11
        }
250 11
    }
251
252
    /**
253
     * Write redirects for expanded redirects.
254
     *
255
     * @param RepeaterPageView $pageView
256
     *
257
     * @since 0.1.1
258
     */
259 2
    private function compileExpandedRedirects(&$pageView)
260
    {
261 2
        $permalinks = $pageView->getRepeaterPermalinks();
262
263
        /** @var ExpandedValue[] $repeaterRedirect */
264 2
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
265
        {
266
            /**
267
             * @var int           $index
268
             * @var ExpandedValue $redirect
269
             */
270
            foreach ($repeaterRedirect as $index => $redirect)
271
            {
272
                $redirectPageView = PageView::createRedirect(
273
                    $redirect->getEvaluated(),
274
                    $permalinks[$index]->getEvaluated(),
275
                    $this->redirectTemplate
276
                );
277
                $this->compilePageView($redirectPageView);
278
            }
279 2
        }
280 2
    }
281
282
    ///
283
    // Twig Functionality
284
    ///
285
286
    /**
287
     * Get the compiled HTML for a specific iteration of a repeater PageView.
288
     *
289
     * @param Twig_Template $template
290
     * @param PageView      $pageView
291
     * @param ExpandedValue $expandedValue
292
     *
293
     * @since  0.1.1
294
     *
295
     * @return string
296
     */
297 2
    private function renderRepeaterPageView(&$template, &$pageView, &$expandedValue)
298
    {
299 2
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
300
301 2
        $pageView->setFrontMatter(array(
302 2
            'permalink' => $expandedValue->getEvaluated(),
303 2
            'iterators' => $expandedValue->getIterators(),
304 2
        ));
305
306
        return $template
307 2
            ->render(array(
308 2
                'this' => $pageView->createJail(),
309 2
            ));
310
    }
311
312
    /**
313
     * Get the compiled HTML for a specific ContentItem.
314
     *
315
     * @param Twig_Template $template
316
     * @param PageView      $pageView
317
     * @param ContentItem   $contentItem
318
     *
319
     * @since  0.1.1
320
     *
321
     * @return string
322
     */
323 1
    private function renderDynamicPageView(&$template, &$pageView, &$contentItem)
324
    {
325 1
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
326
327
        return $template
328 1
            ->render(array(
329 1
                'this' => $contentItem->createJail(),
330 1
            ));
331
    }
332
333
    /**
334
     * Get the compiled HTML for a static PageView.
335
     *
336
     * @param PageView $pageView
337
     *
338
     * @since  0.1.1
339
     *
340
     * @throws \Exception
341
     * @throws \Throwable
342
     * @throws Twig_Error_Syntax
343
     *
344
     * @return string
345
     */
346 10
    private function renderStaticPageView(&$pageView)
347
    {
348 10
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
349
350 10
        return $this
351 10
            ->createTwigTemplate($pageView)
352 10
            ->render(array(
353 10
                'this' => $pageView->createJail(),
354 10
            ));
355
    }
356
357
    /**
358
     * Create a Twig template that just needs an array to render.
359
     *
360
     * @param PageView $pageView The PageView whose body will be used for Twig compilation
361
     *
362
     * @since  0.1.1
363
     *
364
     * @throws \Exception
365
     * @throws \Throwable
366
     * @throws Twig_Error_Syntax
367
     *
368
     * @return Twig_Template
369
     */
370 13
    private function createTwigTemplate(&$pageView)
371
    {
372
        try
373
        {
374 13
            return $this->twig->createTemplate($pageView->getContent());
375
        }
376
        catch (Twig_Error_Syntax $e)
377
        {
378
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
379
            $e->setSourceContext(new Twig_Source(
380
                $pageView->getContent(),
381
                $pageView->getName(),
382
                $pageView->getRelativeFilePath()
383
            ));
384
385
            throw $e;
386
        }
387
    }
388
}
389