Completed
Push — master ( c1cecd...0dc9d2 )
by Vladimir
02:08
created

PageManager::createTwigManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 1
rs 9.4285
c 1
b 0
f 0
1
<?php
2
3
namespace allejo\stakx\Manager;
4
5
use allejo\stakx\Exception\TrackedItemNotFoundException;
6
use allejo\stakx\FrontMatter\ExpandedValue;
7
use allejo\stakx\Object\ContentItem;
8
use allejo\stakx\Object\DynamicPageView;
9
use allejo\stakx\Object\PageView;
10
use allejo\stakx\Object\RepeaterPageView;
11
use allejo\stakx\System\FileExplorer;
12
use allejo\stakx\System\Folder;
13
use Twig_Error_Syntax;
14
use Twig_Template;
15
16
/**
17
 * This class is responsible for handling all of the PageViews within a website.
18
 *
19
 * PageManager will parse all available dynamic and static PageViews. After, dynamic PageViews will be prepared by
20
 * setting the appropriate values for each ContentItem such as permalinks. Lastly, this class will compile all of the
21
 * PageViews and write them to the target directory.
22
 *
23
 * @package allejo\stakx\Manager
24
 */
25
class PageManager extends TrackingManager
26
{
27
    /**
28
     * The relative (to the stakx project) file path to the redirect template
29
     *
30
     * @var string|bool
31
     */
32
    private $redirectTemplate;
33
34
    /**
35
     * @var PageView[]
36
     */
37
    private $twigExtendsDeps;
38
39
    /**
40
     * @var ContentItem[][]
41
     */
42
    private $collections;
43
44
    /**
45
     * @var Folder
46
     */
47
    private $targetDir;
48
49
    private $siteMenu;
50
51
    private $twigOpts;
52
53
    /**
54
     * @var \Twig_Environment
55
     */
56
    private $twig;
57
58
    /**
59
     * PageManager constructor
60
     */
61 1
    public function __construct()
62
    {
63 1
        parent::__construct();
64
65 1
        $this->siteMenu = array();
66 1
    }
67
68
    public function setCollections (&$collections)
69
    {
70
        if (empty($collections)) { return; }
71
72
        $this->collections = &$collections;
73
    }
74
75
    public function setRedirectTemplate ($filePath)
76
    {
77
        $this->redirectTemplate = $filePath;
78
    }
79
80
    /**
81
     * @param Folder $folder The relative target directory as specified from the configuration file
82
     */
83 1
    public function setTargetFolder (&$folder)
84
    {
85 1
        $this->targetDir = &$folder;
86 1
    }
87
88 1
    public function configureTwig ($configuration, $options)
89
    {
90 1
        $this->twigOpts['configuration'] = $configuration;
91 1
        $this->twigOpts['options']       = $options;
92
93 1
        $this->createTwigManager();
94 1
    }
95
96 1
    public function createTwigManager ()
97
    {
98 1
        $twig = new TwigManager();
99 1
        $twig->configureTwig(
100 1
            $this->twigOpts['configuration'],
101 1
            $this->twigOpts['options']
102
        );
103
104 1
        $this->twig = TwigManager::getInstance();
105 1
    }
106
107
    /**
108
     * An array representing the website's menu structure with children and grandchildren made from static PageViews
109
     *
110
     * @return array
111
     */
112
    public function getSiteMenu ()
113
    {
114
        return $this->siteMenu;
115
    }
116
117
    /**
118
     * Go through all of the PageView directories and create a respective PageView for each and classify them as a
119
     * dynamic or static PageView.
120
     *
121
     * @param $pageViewFolders
122
     */
123 1
    public function parsePageViews ($pageViewFolders)
124
    {
125 1
        if (empty($pageViewFolders)) { return; }
126
127
        /**
128
         * The name of the folder where PageViews are located
129
         *
130
         * @var $pageViewFolder string
131
         */
132 1
        foreach ($pageViewFolders as $pageViewFolderName)
133
        {
134 1
            $pageViewFolder = $this->fs->absolutePath($pageViewFolderName);
135
136 1
            if (!$this->fs->exists($pageViewFolder))
137
            {
138
                continue;
139
            }
140
141
            // @TODO Replace this with a regular expression or have wildcard support
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
142 1
            $this->scanTrackableItems($pageViewFolder, array(
143 1
                'refresh' => false,
144
                'fileExplorer' => FileExplorer::INCLUDE_ONLY_FILES
145 1
            ), array('.html', '.twig'));
146 1
            $this->saveFolderDefinition($pageViewFolderName);
147
        }
148 1
    }
149
150
    /**
151
     * Compile dynamic and static PageViews
152
     */
153 1
    public function compileAll ()
154
    {
155 1
        foreach (array_keys($this->trackedItemsFlattened) as $filePath)
156
        {
157 1
            $this->compileFromFilePath($filePath);
158
        }
159 1
    }
160
161
    public function compileSome ($filter = array())
162
    {
163
        /** @var PageView $pageView */
164
        foreach ($this->trackedItemsFlattened as $pageView)
165
        {
166
            if ($pageView->hasTwigDependency($filter['namespace'], $filter['dependency']))
167
            {
168
                $this->compilePageView($pageView);
169
            }
170
        }
171
    }
172
173
    /**
174
     * @param ContentItem $contentItem
175
     */
176
    public function compileContentItem (&$contentItem)
177
    {
178
        $pageView = $contentItem->getPageView();
179
180
        // This ContentItem doesn't have an individual PageView dedicated to displaying this item
181
        if (is_null($pageView))
182
        {
183
            return;
184
        }
185
186
        $template = $this->createTemplate($pageView);
187
        $contentItem->evaluateFrontMatter(
188
            $pageView->getFrontMatter(false)
189
        );
190
191
        $output = $template->render(array(
192
            'this' => $contentItem
193
        ));
194
195
        $this->targetDir->writeFile($contentItem->getTargetFile(), $output);
196
    }
197
198
    /**
199
     * Add a new ContentItem to the respective parent PageView of the ContentItem
200
     *
201
     * @param ContentItem $contentItem
202
     */
203
    public function updatePageView ($contentItem)
204
    {
205
        /** @var DynamicPageView $pageView */
206
        foreach ($this->trackedItems['dynamic'] as &$pageView)
207
        {
208
            $fm = $pageView->getFrontMatter(false);
209
210
            if ($fm['collection'] == $contentItem->getCollection())
211
            {
212
                $pageView->addContentItem($contentItem);
213
            }
214
        }
215
    }
216
217
    /**
218
     * Update an existing Twig variable that's injected globally
219
     *
220
     * @param string $variable
221
     * @param string $value
222
     */
223
    public function updateTwigVariable ($variable, $value)
224
    {
225
        $this->twig->addGlobal($variable, $value);
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 1
    public function isTracked($filePath)
232
    {
233 1
        return (parent::isTracked($filePath) || isset($this->twigExtendsDeps[$filePath]));
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function refreshItem($filePath)
240
    {
241
        if (parent::isTracked($filePath))
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (isTracked() instead of refreshItem()). Are you sure this is correct? If so, you might want to change this to $this->isTracked().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
242
        {
243
            $this->compileFromFilePath($filePath, true);
244
245
            return;
246
        }
247
248
        $this->createTwigManager();
249
250
        foreach ($this->twigExtendsDeps[$filePath] as $pageView)
0 ignored issues
show
Bug introduced by
The expression $this->twigExtendsDeps[$filePath] of type object<allejo\stakx\Object\PageView> is not traversable.
Loading history...
251
        {
252
            $this->compilePageView($pageView);
253
        }
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 1
    protected function handleTrackableItem($filePath, $options = array())
260
    {
261 1
        $pageView  = PageView::create($filePath);
262 1
        $namespace = $pageView->getType();
263
264 1
        if ($namespace == PageView::DYNAMIC_TYPE)
265
        {
266
            $frontMatter = $pageView->getFrontMatter(false);
267
            $collection = $frontMatter['collection'];
268
269
            foreach ($this->collections[$collection] as &$item)
270
            {
271
                $item->evaluateFrontMatter($frontMatter);
272
                $pageView->addContentItem($item);
273
            }
274
        }
275
276 1
        $this->addObjectToTracker($pageView, $pageView->getRelativeFilePath(), $namespace);
277 1
        $this->saveTrackerOptions($pageView->getRelativeFilePath(), array(
278 1
            'viewType' => $namespace
279
        ));
280
281 1
        if ($namespace == PageView::STATIC_TYPE)
282
        {
283
            $this->addToSiteMenu($pageView);
284
        }
285 1
    }
286
287
    /**
288
     * Compile a given PageView
289
     *
290
     * @param string $filePath The file path to the PageView to compile
291
     * @param bool   $refresh  When set to true, the PageView will reread its contents
292
     *
293
     * @throws \Exception
294
     */
295 1
    private function compileFromFilePath ($filePath, $refresh = false)
296
    {
297 1
        if (!$this->isTracked($filePath))
298
        {
299
            throw new TrackedItemNotFoundException('PageView not found');
300
        }
301
302
        /** @var DynamicPageView|PageView|RepeaterPageView $pageView */
303 1
        $pageView = &$this->trackedItemsFlattened[$filePath];
304
305 1
        $this->compilePageView($pageView, $refresh);
306 1
    }
307
308
    /**
309
     * @param DynamicPageView|PageView|RepeaterPageView $pageView
310
     * @param bool                                      $refresh
311
     */
312 1
    private function compilePageView ($pageView, $refresh = false)
313
    {
314 1
        if ($refresh)
315
        {
316
            $pageView->refreshFileContent();
317
        }
318
319 1
        switch ($pageView->getType())
320
        {
321 1
            case PageView::REPEATER_TYPE:
322 1
                $this->compileRepeaterPageView($pageView);
0 ignored issues
show
Compatibility introduced by
$pageView of type object<allejo\stakx\Object\PageView> is not a sub-type of object<allejo\stakx\Object\RepeaterPageView>. It seems like you assume a child class of the class allejo\stakx\Object\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...
323 1
                $this->compileExpandedRedirects($pageView);
324 1
                break;
325
326
            case PageView::DYNAMIC_TYPE:
327
                $this->compileDynamicPageView($pageView);
328
                $this->compileNormalRedirects($pageView);
329
                break;
330
331
            case PageView::STATIC_TYPE:
332
                $this->compileStaticPageView($pageView);
333
                $this->compileNormalRedirects($pageView);
334
                break;
335
        }
336 1
    }
337
338
    /**
339
     * @param RepeaterPageView $pageView
340
     */
341 1
    private function compileRepeaterPageView (&$pageView)
342
    {
343 1
        $template = $this->createTemplate($pageView);
344 1
        $pageView->rewindPermalink();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class allejo\stakx\Object\PageView as the method rewindPermalink() does only exist in the following sub-classes of allejo\stakx\Object\PageView: allejo\stakx\Object\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...
345
346 1
        foreach ($pageView->getRepeaterPermalinks() as $permalink)
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class allejo\stakx\Object\PageView as the method getRepeaterPermalinks() does only exist in the following sub-classes of allejo\stakx\Object\PageView: allejo\stakx\Object\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...
347
        {
348 1
            $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\Object\PageView as the method bumpPermalink() does only exist in the following sub-classes of allejo\stakx\Object\PageView: allejo\stakx\Object\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...
349 1
            $pageView->setFrontMatter(array(
350 1
                'permalink' => $permalink->getEvaluated(),
351 1
                'iterators' => $permalink->getIterators()
352
            ));
353
354 1
            $output = $template->render(array(
355 1
                'this' => $pageView->createJail()
356
            ));
357
358 1
            $this->output->notice("Writing repeater file: {file}", array('file' => $pageView->getTargetFile()));
359 1
            $this->targetDir->writeFile($pageView->getTargetFile(), $output);
360
        }
361 1
    }
362
363
    /**
364
     * @param PageView $pageView
365
     */
366
    private function compileDynamicPageView (&$pageView)
367
    {
368
        $template = $this->createTemplate($pageView);
369
370
        $pageViewFrontMatter = $pageView->getFrontMatter(false);
371
        $collection = $pageViewFrontMatter['collection'];
372
373
        /** @var ContentItem $contentItem */
374
        foreach ($this->collections[$collection] as &$contentItem)
375
        {
376
            $output = $template->render(array(
377
                'this' => $contentItem->createJail()
378
            ));
379
380
            $this->output->notice("Writing file: {file}", array('file' => $contentItem->getTargetFile()));
381
            $this->targetDir->writeFile($contentItem->getTargetFile(), $output);
382
        }
383
    }
384
385
    /**
386
     * @param PageView $pageView
387
     */
388
    private function compileStaticPageView (&$pageView)
389
    {
390
        $this->twig->addGlobal('__currentTemplate', $pageView->getFilePath());
391
392
        $template = $this->createTemplate($pageView);
393
        $output = $template->render(array(
394
            'this' => $pageView->createJail()
395
        ));
396
397
        $this->output->notice("Writing file: {file}", array('file' => $pageView->getTargetFile()));
398
        $this->targetDir->writeFile($pageView->getTargetFile(), $output);
399
    }
400
401
    /**
402
     * @param DynamicPageView|PageView $pageView
403
     */
404
    private function compileNormalRedirects (&$pageView)
405
    {
406
        foreach ($pageView->getRedirects() as $redirect)
0 ignored issues
show
Bug introduced by
The expression $pageView->getRedirects() 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...
407
        {
408
            $redirectPageView = PageView::createRedirect(
409
                $redirect,
410
                $pageView->getPermalink(),
411
                $this->redirectTemplate
412
            );
413
414
            $this->compilePageView($redirectPageView);
415
        }
416
    }
417
418
    /**
419
     * @param RepeaterPageView $pageView
420
     */
421 1
    private function compileExpandedRedirects (&$pageView)
422
    {
423 1
        $permalinks = $pageView->getRepeaterPermalinks();
424
425
        /** @var ExpandedValue[] $repeaterRedirect */
426 1
        foreach ($pageView->getRepeaterRedirects() as $repeaterRedirect)
427
        {
428
            /**
429
             * @var int           $index
430
             * @var ExpandedValue $redirect
431
             */
432
            foreach ($repeaterRedirect as $index => $redirect)
433
            {
434
                $redirectPageView = PageView::createRedirect(
435
                    $redirect->getEvaluated(),
436
                    $permalinks[$index]->getEvaluated(),
437
                    $this->redirectTemplate
438
                );
439
440
                $this->compilePageView($redirectPageView);
441
            }
442
        }
443 1
    }
444
445
    /**
446
     * Add a static PageView to the menu array. Dynamic PageViews are not added to the menu
447
     *
448
     * @param PageView $pageView
449
     */
450
    private function addToSiteMenu (&$pageView)
451
    {
452
        $frontMatter = $pageView->getFrontMatter();
453
454
        if (!array_key_exists('permalink', $frontMatter) ||
455
            (array_key_exists('menu', $frontMatter) && !$frontMatter['menu']))
456
        {
457
            return;
458
        }
459
460
        $url = $pageView->getPermalink();
461
        $root = &$this->siteMenu;
462
        $permalink = trim($url, DIRECTORY_SEPARATOR);
463
        $dirs = explode(DIRECTORY_SEPARATOR, $permalink);
464
465
        while (count($dirs) > 0)
466
        {
467
            $name = array_shift($dirs);
468
            $name = (!empty($name)) ? $name : '.';
469
470
            if (!is_null($name) && count($dirs) == 0)
471
            {
472
                $children = array();
473
474
                if (array_key_exists($name, $root) && is_array($root[$name]))
475
                {
476
                    $children = $root[$name]['children'];
477
                }
478
479
                $root[$name] = &$pageView;
480
                $root = &$root[$name]->getChildren();
481
482
                if (!empty($children))
483
                {
484
                    $root = $children;
485
                }
486
            }
487
            else
488
            {
489
                $root[$name]['children'] = array();
490
                $root = &$root[$name]['children'];
491
            }
492
        }
493
    }
494
495
    /**
496
     * @param PageView $pageView
497
     *
498
     * @return Twig_Template
499
     * @throws Twig_Error_Syntax
500
     */
501 1
    private function createTemplate (&$pageView)
502
    {
503
        try
504
        {
505 1
            $template = $this->twig->createTemplate($pageView->getContent());
506
507 1
            $this->trackParentTwigTemplate($template, $pageView);
508
509 1
            return $template;
510
        }
511
        catch (Twig_Error_Syntax $e)
512
        {
513
            $e->setTemplateLine($e->getTemplateLine() + $pageView->getLineOffset());
514
            $e->setTemplateName($pageView->getRelativeFilePath());
515
516
            throw $e;
517
        }
518
    }
519
520
    /**
521
     * Find the parent Twig templates of the given template and keep a list of it
522
     *
523
     * @param Twig_Template $template The template created from the PageView's content
524
     * @param PageView      $pageView The PageView that has this content. Used to keep a reference of PageViews
525
     */
526 1
    private function trackParentTwigTemplate ($template, &$pageView)
527
    {
528 1
        if (!$this->tracking) { return; }
529
530
        /** @var Twig_Template $parent */
531
        $parent = $template->getParent(array());
532
533
        while ($parent !== false)
534
        {
535
            $filePath = $this->fs->getRelativePath($parent->getSourceContext()->getPath());
536
537
            $this->twigExtendsDeps[$filePath][(string)$pageView->getFilePath()] = &$pageView;
538
            $parent = $parent->getParent(array());
539
        }
540
    }
541
}