Completed
Push — master ( 3a56df...caf2ba )
by Vladimir
02:43
created

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