Completed
Push — master ( 89bfcf...4ab803 )
by Vladimir
02:41
created

Compiler   C

Complexity

Total Complexity 44

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 63.58%

Importance

Changes 0
Metric Value
wmc 44
c 0
b 0
f 0
lcom 1
cbo 18
dl 0
loc 478
rs 5.9076
ccs 96
cts 151
cp 0.6358

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A setRedirectTemplate() 0 4 1
A setTargetFolder() 0 4 1
A setPageViews() 0 5 1
A setThemeName() 0 4 1
A isImportDependency() 0 4 1
A isParentTemplate() 0 4 1
A refreshParent() 0 7 2
A getTemplateMappings() 0 4 1
A compileAll() 0 7 2
A compileImportDependencies() 0 7 2
A compileSome() 0 14 4
B compilePageView() 0 39 5
A compileStaticPageView() 0 8 1
B compileDynamicPageViews() 0 23 4
A compileRepeaterPageViews() 0 17 2
A compileContentItem() 0 14 1
A compileStandardRedirects() 0 15 2
A compileExpandedRedirects() 0 22 3
A renderRepeaterPageView() 0 12 1
A renderDynamicPageView() 0 7 1
A renderStaticPageView() 0 8 1
B createTwigTemplate() 0 45 5

How to fix   Complexity   

Complex Class

Complex classes like Compiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Compiler, and based on these observations, apply Extract Interface, too.

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\Document\TwigDocument;
16
use allejo\stakx\Exception\FileAwareException;
17
use allejo\stakx\FrontMatter\ExpandedValue;
18
use allejo\stakx\Manager\BaseManager;
19
use allejo\stakx\Manager\ThemeManager;
20
use allejo\stakx\Manager\TwigManager;
21
use allejo\stakx\System\Folder;
22
use allejo\stakx\System\FilePath;
23
use Twig_Environment;
24
use Twig_Error_Runtime;
25
use Twig_Error_Syntax;
26
use Twig_Source;
27
use Twig_Template;
28
29
/**
30
 * This class takes care of rendering the Twig body of PageViews with the respective information and it also takes care
31
 * of writing the rendered Twig to the filesystem.
32
 *
33
 * @internal
34
 *
35
 * @since 0.1.1
36
 */
37
class Compiler extends BaseManager
38
{
39
    /** @var string|false */
40
    private $redirectTemplate;
41
42
    /** @var PageView[][] */
43
    private $importDependencies;
44
45
    /** @var Twig_Template[] */
46
    private $templateDependencies;
47
48
    /** @var PageView[] */
49
    private $pageViewsFlattened;
50
51
    /** @var string[] */
52
    private $templateMapping;
53
54
    /** @var PageView[][] */
55
    private $pageViews;
56
57
    /** @var Folder */
58
    private $folder;
59
60
    /** @var string */
61
    private $theme;
62
63
    /** @var Twig_Environment */
64
    private $twig;
65
66 14
    public function __construct()
67
    {
68 14
        parent::__construct();
69
70 14
        $this->twig = TwigManager::getInstance();
71 14
        $this->theme = '';
72 14
    }
73
74
    /**
75
     * @param string|false $template
76
     */
77
    public function setRedirectTemplate($template)
78
    {
79
        $this->redirectTemplate = $template;
80
    }
81
82
    /**
83
     * @param Folder $folder
84
     */
85 14
    public function setTargetFolder(Folder $folder)
86
    {
87 14
        $this->folder = $folder;
88 14
    }
89
90
    /**
91
     * @param PageView[][] $pageViews
92
     * @param PageView[]   $pageViewsFlattened
93
     */
94 14
    public function setPageViews(array &$pageViews, array &$pageViewsFlattened)
95
    {
96 14
        $this->pageViews = &$pageViews;
97 14
        $this->pageViewsFlattened = &$pageViewsFlattened;
98 14
    }
99
100
    /**
101
     * @param string $themeName
102
     */
103
    public function setThemeName($themeName)
104
    {
105
        $this->theme = $themeName;
106
    }
107
108
    ///
109
    // Twig parent templates
110
    ///
111
112
    public function isImportDependency($filePath)
113
    {
114
        return isset($this->importDependencies[$filePath]);
115
    }
116
117
    /**
118
     * Check whether a given file path is used as a parent template by a PageView
119
     *
120
     * @param  string $filePath
121
     *
122
     * @return bool
123
     */
124
    public function isParentTemplate($filePath)
125
    {
126
        return isset($this->templateDependencies[$filePath]);
127
    }
128
129
    /**
130
     * Rebuild all of the PageViews that used a given template as a parent
131
     *
132
     * @param string $filePath The file path to the parent Twig template
133
     */
134
    public function refreshParent($filePath)
135
    {
136
        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...
137
        {
138
            $this->compilePageView($parentTemplate);
139
        }
140
    }
141
142
    public function getTemplateMappings()
143
    {
144
        return $this->templateMapping;
145
    }
146
147
    ///
148
    // IO Functionality
149
    ///
150
151
    /**
152
     * Compile all of the PageViews registered with the compiler.
153
     *
154
     * @since 0.1.0
155
     */
156 14
    public function compileAll()
157
    {
158 14
        foreach ($this->pageViewsFlattened as &$pageView)
159
        {
160 14
            $this->compilePageView($pageView);
161
        }
162 14
    }
163
164
    public function compileImportDependencies($filePath)
165
    {
166
        foreach ($this->importDependencies[$filePath] as &$dependent)
167
        {
168
            $this->compilePageView($dependent);
169
        }
170
    }
171
172
    public function compileSome(array $filter = array())
173
    {
174
        /** @var PageView $pageView */
175
        foreach ($this->pageViewsFlattened as &$pageView)
176
        {
177
            $ns = $filter['namespace'];
178
179
            if ($pageView->hasTwigDependency($ns, $filter['dependency']) ||
180
                $pageView->hasTwigDependency($ns, null)
181
            ) {
182
                $this->compilePageView($pageView);
183
            }
184
        }
185
    }
186
187
    /**
188
     * Compile an individual PageView item.
189
     *
190
     * This function will take care of determining *how* to treat the PageView and write the compiled output to a the
191
     * respective target file.
192
     *
193
     * @param DynamicPageView|RepeaterPageView|PageView $pageView The PageView that needs to be compiled
194
     *
195
     * @since 0.1.1
196
     */
197 14
    public function compilePageView(PageView &$pageView)
198
    {
199 14
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
200 14
        $this->output->debug('Compiling {type} PageView: {pageview}', array(
201 14
            'pageview' => $pageView->getRelativeFilePath(),
202 14
            'type' => $pageView->getType()
203
        ));
204
205
        try
206
        {
207 14
            switch ($pageView->getType())
208
            {
209 14
                case PageView::STATIC_TYPE:
210 10
                    $this->compileStaticPageView($pageView);
211 10
                    $this->compileStandardRedirects($pageView);
212 10
                    break;
213
214 4
                case PageView::DYNAMIC_TYPE:
215 2
                    $this->compileDynamicPageViews($pageView);
216 2
                    $this->compileStandardRedirects($pageView);
217 2
                    break;
218
219 2
                case PageView::REPEATER_TYPE:
220 2
                    $this->compileRepeaterPageViews($pageView);
221 2
                    $this->compileExpandedRedirects($pageView);
222 14
                    break;
223
            }
224
        }
225
        catch (Twig_Error_Runtime $e)
226
        {
227
            throw new FileAwareException(
228
                $e->getRawMessage(),
229
                $e->getCode(),
230
                $e,
231
                $pageView->getRelativeFilePath(),
232
                $e->getTemplateLine() + $pageView->getLineOffset()
233
            );
234
        }
235 14
    }
236
237
    /**
238
     * Write the compiled output for a static PageView.
239
     *
240
     * @param PageView $pageView
241
     *
242
     * @since 0.1.1
243
     */
244 10
    private function compileStaticPageView(PageView &$pageView)
245
    {
246 10
        $targetFile = $pageView->getTargetFile();
247 10
        $output = $this->renderStaticPageView($pageView);
248
249 10
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
250 10
        $this->folder->writeFile($targetFile, $output);
251 10
    }
252
253
    /**
254
     * Write the compiled output for a dynamic PageView.
255
     *
256
     * @param DynamicPageView $pageView
257
     *
258
     * @since 0.1.1
259
     */
260 2
    private function compileDynamicPageViews(DynamicPageView &$pageView)
261
    {
262 2
        $contentItems = $pageView->getRepeatableItems();
263 2
        $template = $this->createTwigTemplate($pageView);
264
265 2
        foreach ($contentItems as &$contentItem)
266
        {
267 2
            if ($contentItem->isDraft() && !Service::getParameter(BuildableCommand::USE_DRAFTS))
268
            {
269 1
                $this->output->debug('{file}: marked as a draft', array(
270 1
                    'file' => $contentItem->getRelativeFilePath()
271
                ));
272
273 1
                continue;
274
            }
275
276 2
            $targetFile = $contentItem->getTargetFile();
277 2
            $output = $this->renderDynamicPageView($template, $contentItem);
0 ignored issues
show
Documentation introduced by
$contentItem is of type object<allejo\stakx\Document\RepeatableItem>, but the function expects a object<allejo\stakx\Document\TwigDocument>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
278
279 2
            $this->output->notice('Writing file: {file}', array('file' => $targetFile));
280 2
            $this->folder->writeFile($targetFile, $output);
281
        }
282 2
    }
283
284
    /**
285
     * Write the compiled output for a repeater PageView.
286
     *
287
     * @param RepeaterPageView $pageView
288
     *
289
     * @since 0.1.1
290
     */
291 2
    private function compileRepeaterPageViews(RepeaterPageView &$pageView)
292
    {
293 2
        $pageView->rewindPermalink();
294
295 2
        $template = $this->createTwigTemplate($pageView);
296 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...
297
298 2
        foreach ($permalinks as $permalink)
299
        {
300 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...
301 2
            $targetFile = $pageView->getTargetFile();
302 2
            $output = $this->renderRepeaterPageView($template, $pageView, $permalink);
0 ignored issues
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...
303
304 2
            $this->output->notice('Writing repeater file: {file}', array('file' => $targetFile));
305 2
            $this->folder->writeFile($targetFile, $output);
306
        }
307 2
    }
308
309
    /**
310
     * @deprecated
311
     *
312
     * @todo This function needs to be rewritten or removed. Something
313
     *
314
     * @param ContentItem $contentItem
315
     */
316
    public function compileContentItem(ContentItem &$contentItem)
317
    {
318
        $pageView = &$contentItem->getPageView();
319
        $template = $this->createTwigTemplate($pageView);
320
321
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
322
        $contentItem->evaluateFrontMatter($pageView->getFrontMatter(false));
323
324
        $targetFile = $contentItem->getTargetFile();
325
        $output = $this->renderDynamicPageView($template, $contentItem);
326
327
        $this->output->notice('Writing file: {file}', array('file' => $targetFile));
328
        $this->folder->writeFile($targetFile, $output);
329
    }
330
331
    ///
332
    // Redirect handling
333
    ///
334
335
    /**
336
     * Write redirects for standard redirects.
337
     *
338
     * @param PageView $pageView
339
     *
340
     * @since 0.1.1
341
     */
342 12
    private function compileStandardRedirects(PageView &$pageView)
343
    {
344 12
        $redirects = $pageView->getRedirects();
345
346 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...
347
        {
348 4
            $redirectPageView = PageView::createRedirect(
349 4
                $redirect,
350 4
                $pageView->getPermalink(),
351 4
                $this->redirectTemplate
352
            );
353
354 4
            $this->compileStaticPageView($redirectPageView);
355
        }
356 12
    }
357
358
    /**
359
     * Write redirects for expanded redirects.
360
     *
361
     * @param RepeaterPageView $pageView
362
     *
363
     * @since 0.1.1
364
     */
365 2
    private function compileExpandedRedirects(RepeaterPageView &$pageView)
366
    {
367 2
        $permalinks = $pageView->getRepeaterPermalinks();
368
369
        /** @var ExpandedValue[] $repeaterRedirect */
370 2
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
371
        {
372
            /**
373
             * @var int           $index
374
             * @var ExpandedValue $redirect
375
             */
376
            foreach ($repeaterRedirect as $index => $redirect)
377
            {
378
                $redirectPageView = PageView::createRedirect(
379
                    $redirect->getEvaluated(),
380
                    $permalinks[$index]->getEvaluated(),
381
                    $this->redirectTemplate
382
                );
383
                $this->compilePageView($redirectPageView);
384
            }
385
        }
386 2
    }
387
388
    ///
389
    // Twig Functionality
390
    ///
391
392
    /**
393
     * Get the compiled HTML for a specific iteration of a repeater PageView.
394
     *
395
     * @param Twig_Template $template
396
     * @param PageView      $pageView
397
     * @param ExpandedValue $expandedValue
398
     *
399
     * @since  0.1.1
400
     *
401
     * @return string
402
     */
403 2
    private function renderRepeaterPageView(Twig_Template &$template, RepeaterPageView &$pageView, ExpandedValue &$expandedValue)
404
    {
405 2
        $pageView->setFrontMatter(array(
406 2
            'permalink' => $expandedValue->getEvaluated(),
407 2
            'iterators' => $expandedValue->getIterators(),
408
        ));
409
410
        return $template
411 2
            ->render(array(
412 2
                'this' => $pageView->createJail(),
413
            ));
414
    }
415
416
    /**
417
     * Get the compiled HTML for a specific ContentItem.
418
     *
419
     * @param Twig_Template $template
420
     * @param TwigDocument  $twigItem
421
     *
422
     * @return string
423
     * @since  0.1.1
424
     *
425
     */
426 2
    private function renderDynamicPageView(Twig_Template &$template, TwigDocument &$twigItem)
427
    {
428
        return $template
429 2
            ->render(array(
430 2
                'this' => $twigItem->createJail(),
431
            ));
432
    }
433
434
    /**
435
     * Get the compiled HTML for a static PageView.
436
     *
437
     * @param PageView $pageView
438
     *
439
     * @since  0.1.1
440
     *
441
     * @throws \Exception
442
     * @throws \Throwable
443
     * @throws Twig_Error_Syntax
444
     *
445
     * @return string
446
     */
447 10
    private function renderStaticPageView(PageView &$pageView)
448
    {
449
        return $this
450 10
            ->createTwigTemplate($pageView)
451 10
            ->render(array(
452 10
                'this' => $pageView->createJail(),
453
            ));
454
    }
455
456
    /**
457
     * Create a Twig template that just needs an array to render.
458
     *
459
     * @param PageView $pageView The PageView whose body will be used for Twig compilation
460
     *
461
     * @since  0.1.1
462
     *
463
     * @throws \Exception
464
     * @throws \Throwable
465
     * @throws Twig_Error_Syntax
466
     *
467
     * @return Twig_Template
468
     */
469 14
    private function createTwigTemplate(PageView &$pageView)
470
    {
471
        try
472
        {
473 14
            $template = $this->twig->createTemplate($pageView->getContent());
474
475 14
            $this->templateMapping[$template->getTemplateName()] = $pageView->getRelativeFilePath();
476
477 14
            if (Service::getParameter(BuildableCommand::WATCHING))
478
            {
479
                // Keep track of import dependencies
480
                foreach ($pageView->getImportDependencies() as $dependency)
481
                {
482
                    $this->importDependencies[$dependency][$pageView->getName()] = &$pageView;
0 ignored issues
show
Deprecated Code introduced by
The method allejo\stakx\FrontMatter...tterDocument::getName() has been deprecated with message: use getObjectName() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
483
                }
484
485
                // Keep track of Twig extends'
486
                $parent = $template->getParent(array());
487
488
                while ($parent !== false)
489
                {
490
                    // Replace the '@theme' namespace in Twig with the path to the theme folder and create a UnixFilePath object from the given path
491
                    $path = str_replace('@theme', $this->fs->appendPath(ThemeManager::THEME_FOLDER, $this->theme), $parent->getTemplateName());
492
                    $path = new FilePath($path);
493
494
                    $this->templateDependencies[(string)$path][$pageView->getName()] = &$pageView;
0 ignored issues
show
Deprecated Code introduced by
The method allejo\stakx\FrontMatter...tterDocument::getName() has been deprecated with message: use getObjectName() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
495
496
                    $parent = $parent->getParent(array());
497
                }
498
            }
499
500 14
            return $template;
501
        }
502
        catch (Twig_Error_Syntax $e)
503
        {
504
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
505
            $e->setSourceContext(new Twig_Source(
506
                $pageView->getContent(),
507
                $pageView->getName(),
0 ignored issues
show
Deprecated Code introduced by
The method allejo\stakx\FrontMatter...tterDocument::getName() has been deprecated with message: use getObjectName() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
508
                $pageView->getRelativeFilePath()
509
            ));
510
511
            throw $e;
512
        }
513
    }
514
}
515