Completed
Pull Request — master (#41)
by Vladimir
02:31
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\FrontMatter\ExpandedValue;
11
use allejo\stakx\Manager\BaseManager;
12
use allejo\stakx\Manager\TwigManager;
13
use allejo\stakx\Object\ContentItem;
14
use allejo\stakx\Object\DynamicPageView;
15
use allejo\stakx\Object\PageView;
16
use allejo\stakx\Object\RepeaterPageView;
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 $pageViews;
38
39
    /** @var Folder */
40
    private $folder;
41
42
    /** @var Twig_Environment */
43
    private $twig;
44
45 9
    public function __construct()
46
    {
47 9
        parent::__construct();
48
49 9
        $this->twig = TwigManager::getInstance();
50 9
    }
51
52
    /**
53
     * @param string|false $template
54
     */
55
    public function setRedirectTemplate($template)
56
    {
57
        $this->redirectTemplate = $template;
58
    }
59
60
    /**
61
     * @param Folder $folder
62
     */
63 9
    public function setTargetFolder(Folder $folder)
64
    {
65 9
        $this->folder = $folder;
66 9
    }
67
68
    /**
69
     * @param PageView[] $pageViews
70
     */
71 9
    public function setPageViews(array &$pageViews)
72
    {
73 9
        $this->pageViews = &$pageViews;
74 9
    }
75
76
    ///
77
    // IO Functionality
78
    ///
79
80
    /**
81
     * Compile all of the PageViews registered with the compiler.
82
     *
83
     * @since 0.1.0
84
     */
85 9
    public function compileAll()
86
    {
87 9
        $pageViews = array(PageView::REPEATER_TYPE, PageView::DYNAMIC_TYPE, PageView::STATIC_TYPE);
88
89 9
        foreach ($pageViews as $type)
90
        {
91 9
            foreach ($this->pageViews[$type] as &$pageView)
0 ignored issues
show
Bug introduced by
The expression $this->pageViews[$type] of type object<allejo\stakx\Object\PageView> is not traversable.
Loading history...
92
            {
93 9
                $this->compilePageView($pageView);
94 9
            }
95 9
        }
96 9
    }
97
98
    /**
99
     * Compile an individual PageView item.
100
     *
101
     * This function will take care of determining *how* to treat the PageView and write the compiled output to a the
102
     * respective target file.
103
     *
104
     * @param DynamicPageView|RepeaterPageView|PageView $pageView The PageView that needs to be compiled
105
     *
106
     * @since 0.1.1
107
     */
108 9
    private function compilePageView(&$pageView)
109
    {
110 9
        switch ($pageView->getType())
111
        {
112 9
            case PageView::STATIC_TYPE:
113 9
                $this->compileStaticPageView($pageView);
114 9
                $this->compileStandardRedirects($pageView);
115 9
                break;
116
117
            case PageView::DYNAMIC_TYPE:
118
                $this->compileDynamicPageViews($pageView);
0 ignored issues
show
Compatibility introduced by
$pageView of type object<allejo\stakx\Object\PageView> is not a sub-type of object<allejo\stakx\Object\DynamicPageView>. It seems like you assume a child class of the class allejo\stakx\Object\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...
119
                $this->compileStandardRedirects($pageView);
120
                break;
121
122
            case PageView::REPEATER_TYPE:
123 1
                $this->compileRepeaterPageViews($pageView);
0 ignored issues
show
Compatibility introduced by
$pageView of type object<allejo\stakx\Object\PageView> is not a sub-type of object<allejo\stakx\Object\RepeaterPageView>. It seems like you assume a child class of the class allejo\stakx\Object\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...
124
                $this->compileExpandedRedirects($pageView);
125
                break;
126 9
        }
127 9
    }
128
129
    /**
130
     * Write the compiled output for a static PageView.
131
     *
132
     * @param PageView $pageView
133
     *
134
     * @since 0.1.1
135
     */
136 9
    private function compileStaticPageView(&$pageView)
137
    {
138 9
        $targetFile = $pageView->getTargetFile();
139 9
        $output = $this->renderStaticPageView($pageView);
140
141 9
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
142 9
        $this->folder->writeFile($targetFile, $output);
143 9
    }
144
145
    /**
146
     * Write the compiled output for a dynamic PageView.
147
     *
148
     * @param DynamicPageView $pageView
149
     *
150
     * @since 0.1.1
151
     */
152 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...
153
    {
154
        $contentItems = $pageView->getContentItems();
155
        $template = $this->createTwigTemplate($pageView);
156
157
        foreach ($contentItems as $contentItem)
158
        {
159
            $targetFile = $contentItem->getTargetFile();
160
            $output = $this->renderDynamicPageView($template, $pageView, $contentItem);
161
162
            $this->output->notice('Writing file: {file}', array('file' => $targetFile));
163
            $this->folder->writeFile($targetFile, $output);
164
        }
165
    }
166
167
    /**
168
     * Write the compiled output for a repeater PageView.
169
     *
170
     * @param RepeaterPageView $pageView
171
     *
172
     * @since 0.1.1
173
     */
174 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...
175
    {
176
        $pageView->rewindPermalink();
177
178
        $template = $this->createTwigTemplate($pageView);
179
        $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\Object\PageView as the method getRepeaterPermalinks() does only exist in the following sub-classes of allejo\stakx\Object\PageView: allejo\stakx\Object\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...
180
181
        foreach ($permalinks as $permalink)
182
        {
183
            $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\Object\PageView as the method bumpPermalink() does only exist in the following sub-classes of allejo\stakx\Object\PageView: allejo\stakx\Object\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...
184
            $targetFile = $pageView->getTargetFile();
185
            $output = $this->renderRepeaterPageView($template, $pageView, $permalink);
186
187
            $this->output->notice('Writing repeater file: {file}', array('file' => $targetFile));
188
            $this->folder->writeFile($targetFile, $output);
189
        }
190
    }
191
192
    ///
193
    // Redirect handling
194
    ///
195
196
    /**
197
     * Write redirects for standard redirects.
198
     *
199
     * @param PageView $pageView
200
     *
201
     * @since 0.1.1
202
     */
203 9
    private function compileStandardRedirects(&$pageView)
204
    {
205 9
        $redirects = $pageView->getRedirects();
206
207 9
        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...
208
        {
209 4
            $redirectPageView = PageView::createRedirect(
210 4
                $redirect,
211 4
                $pageView->getPermalink(),
212 4
                $this->redirectTemplate
213 4
            );
214
215 4
            $this->compileStaticPageView($redirectPageView);
216 9
        }
217 9
    }
218
219
    /**
220
     * Write redirects for expanded redirects.
221
     *
222
     * @param RepeaterPageView $pageView
223
     *
224
     * @since 0.1.1
225
     */
226
    private function compileExpandedRedirects(&$pageView)
227
    {
228
        $permalinks = $pageView->getRepeaterPermalinks();
229
230
        /** @var ExpandedValue[] $repeaterRedirect */
231
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
232
        {
233
            /**
234
             * @var int           $index
235
             * @var ExpandedValue $redirect
236
             */
237
            foreach ($repeaterRedirect as $index => $redirect)
238
            {
239
                $redirectPageView = PageView::createRedirect(
240
                    $redirect->getEvaluated(),
241
                    $permalinks[$index]->getEvaluated(),
242
                    $this->redirectTemplate
243
                );
244
                $this->compilePageView($redirectPageView);
245
            }
246
        }
247
    }
248
249
    ///
250
    // Twig Functionality
251
    ///
252
253
    /**
254
     * Get the compiled HTML for a specific iteration of a repeater PageView.
255
     *
256
     * @param Twig_Template $template
257
     * @param PageView      $pageView
258
     * @param ExpandedValue $expandedValue
259
     *
260
     * @since  0.1.1
261
     *
262
     * @return string
263
     */
264
    private function renderRepeaterPageView(&$template, &$pageView, &$expandedValue)
265
    {
266
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
267
268
        $pageView->setFrontMatter(array(
269
            'permalink' => $expandedValue->getEvaluated(),
270
            'iterators' => $expandedValue->getIterators(),
271
        ));
272
273
        return $template
274
            ->render(array(
275
                'this' => $pageView->createJail(),
276
            ));
277
    }
278
279
    /**
280
     * Get the compiled HTML for a specific ContentItem.
281
     *
282
     * @param Twig_Template $template
283
     * @param PageView      $pageView
284
     * @param ContentItem   $contentItem
285
     *
286
     * @since  0.1.1
287
     *
288
     * @return string
289
     */
290
    private function renderDynamicPageView(&$template, &$pageView, &$contentItem)
291
    {
292
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
293
294
        return $template
295
            ->render(array(
296
                'this' => $contentItem->createJail(),
297
            ));
298
    }
299
300
    /**
301
     * Get the compiled HTML for a static PageView.
302
     *
303
     * @param PageView $pageView
304
     *
305
     * @since  0.1.1
306
     *
307
     * @throws \Exception
308
     * @throws \Throwable
309
     * @throws Twig_Error_Syntax
310
     *
311
     * @return string
312
     */
313 9
    private function renderStaticPageView(&$pageView)
314
    {
315 9
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
316
317 9
        return $this
318 9
            ->createTwigTemplate($pageView)
319 9
            ->render(array(
320 9
                'this' => $pageView->createJail(),
321 9
            ));
322
    }
323
324
    /**
325
     * Create a Twig template that just needs an array to render.
326
     *
327
     * @param PageView $pageView The PageView whose body will be used for Twig compilation
328
     *
329
     * @since  0.1.1
330
     *
331
     * @throws \Exception
332
     * @throws \Throwable
333
     * @throws Twig_Error_Syntax
334
     *
335
     * @return Twig_Template
336
     */
337 9
    private function createTwigTemplate(&$pageView)
338
    {
339
        try
340
        {
341 9
            return $this->twig->createTemplate($pageView->getContent());
342
        }
343
        catch (Twig_Error_Syntax $e)
344
        {
345
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
346
            $e->setSourceContext(new Twig_Source(
347
                $pageView->getContent(),
348
                $pageView->getName(),
349
                $pageView->getRelativeFilePath()
350
            ));
351
352
            throw $e;
353
        }
354
    }
355
}
356