Completed
Push — master ( b785dd...3df4eb )
by Vladimir
03:46
created

Compiler::compileExpandedRedirects()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5.0975

Importance

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