Completed
Pull Request — master (#46)
by Vladimir
02:44
created

Compiler::compileContentItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 13
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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