Completed
Push — master ( c2b5cc...89bfcf )
by Vladimir
03:33
created

Compiler::refreshParent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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