Completed
Push — master ( 808f8c...952fde )
by Vladimir
11s
created

Website::watch()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 0
1
<?php
2
3
/**
4
 * @copyright 2018 Vladimir Jimenez
5
 * @license   https://github.com/stakx-io/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx;
9
10
use allejo\stakx\Command\BuildableCommand;
11
use allejo\stakx\Core\StakxLogger;
12
use allejo\stakx\Event\BuildProcessComplete;
13
use allejo\stakx\Exception\FileAwareException;
14
use allejo\stakx\Filesystem\FileExplorer;
15
use allejo\stakx\Filesystem\FilesystemLoader as fs;
16
use allejo\stakx\Filesystem\Folder;
17
use allejo\stakx\Manager\AssetManager;
18
use allejo\stakx\Manager\CollectionManager;
19
use allejo\stakx\Manager\DataManager;
20
use allejo\stakx\Manager\PageManager;
21
use allejo\stakx\Manager\ThemeManager;
22
use allejo\stakx\Manager\TwigManager;
23
use Highlight\Highlighter;
24
use Kwf\FileWatcher\Event\AbstractEvent;
25
use Kwf\FileWatcher\Event\Create;
26
use Kwf\FileWatcher\Event\Modify;
27
use Kwf\FileWatcher\Event\Move;
28
use Kwf\FileWatcher\Watcher;
29
use Symfony\Component\DependencyInjection\ContainerInterface;
30
use Symfony\Component\EventDispatcher\EventDispatcher;
31
32
class Website
33
{
34
    /**
35
     * The location of where the compiled website will be written to.
36
     *
37
     * @var Folder
38
     */
39
    private $outputDirectory;
40
41
    /**
42
     * When set to true, the Stakx website will be built without a configuration file.
43
     *
44
     * @var bool
45
     */
46
    private $confLess;
47
48
    /**
49
     * @var StakxLogger
50
     */
51
    private $output;
52
53
    /**
54
     * @var AssetManager
55
     */
56
    private $am;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $am. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
57
58
    /**
59
     * @var CollectionManager
60
     */
61
    private $cm;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $cm. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
62
63
    /**
64
     * @var DataManager
65
     */
66
    private $dm;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $dm. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
67
68
    /**
69
     * @var PageManager
70
     */
71
    private $pm;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $pm. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
72
73
    /**
74
     * @var ThemeManager
75
     */
76
    private $tm;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $tm. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
77
78
    /** @var Compiler */
79
    private $compiler;
80
81
    /** @var array */
82
    private $creationQueue;
83
84
    private $container;
85
86
    /**
87
     * Constructor.
88
     */
89
    public function __construct(ContainerInterface $container)
90
    {
91
        $this->container = $container;
92
93
        $this->creationQueue = [];
94
        $this->output = $container->get('logger');
95
    }
96
97
    /**
98
     * Compile the website.
99
     *
100
     * @param bool $tracking Whether or not to keep track of files as they're compiled to save time in 'watch'
101
     */
102
    public function build($tracking = false)
103
    {
104
        $logger = $this->container->get('logger');
105
        $conf = $this->container->get(Configuration::class);
106
107
        if (empty($conf->getPageViewFolders()))
108
        {
109
            $logger->error('No PageViews were configured for this site. Check the `pageviews` key in your _config.yml.');
110
111
            return false;
112
        }
113
114
        Service::setParameter(BuildableCommand::WATCHING, $tracking);
115
116
        // Configure the environment
117
        $this->createFolderStructure();
118
        $this->configureHighlighter();
119
120
        // Our output directory
121
        $this->outputDirectory = new Folder($this->getConfiguration()->getTargetFolder());
122
        $this->outputDirectory->setTargetDirectory($this->getConfiguration()->getBaseUrl());
123
124
        $templateEngine = $this->container->get('templating');
125
126
        // Compile everything
127
        $theme = $this->getConfiguration()->getTheme();
128
129
        $this->compiler = $this->container->get('compiler');
130
        $this->compiler->setTargetFolder($this->outputDirectory);
131
        $this->compiler->setThemeName($theme);
132
        $this->compiler->compileAll();
133
134
        if (Service::getParameter(BuildableCommand::BUILD_PROFILE))
135
        {
136
            if (!$templateEngine->hasProfiler())
137
            {
138
                $logger->writeln('This template engine currently does not support a profiler.');
139
            }
140
            else
141
            {
142
                $profilerText = $templateEngine->getProfilerOutput($this->compiler);
143
                $logger->writeln($profilerText);
144
            }
145
        }
146
147
        // At this point, we are looking at static files to copy over meaning we need to ignore all of the files that
148
        // make up the source of a stakx website
149
        $assetsToIgnore = array_merge(
150
            Configuration::$stakxSourceFiles,
151
            $this->getConfiguration()->getExcludes()
152
        );
153
154
        //
155
        // Theme Management
156
        //
157
        if (!is_null($theme))
158
        {
159
            $logger->notice("Looking for '${theme}' theme...");
160
161
            $this->tm = new ThemeManager($theme, $this->container->get('event_dispatcher'), $this->container->get('logger'));
162
            $this->tm->configureFinder($this->getConfiguration()->getIncludes(), $assetsToIgnore);
163
            $this->tm->setFolder($this->outputDirectory);
164
            $this->tm->copyFiles();
165
        }
166
167
        //
168
        // Static file management
169
        //
170
        $this->am = $this->container->get(AssetManager::class);
171
        $this->am->configureFinder($this->getConfiguration()->getIncludes(), $assetsToIgnore);
172
        $this->am->setFolder($this->outputDirectory);
173
        $this->am->copyFiles();
174
175
        /** @var EventDispatcher $dispatcher */
176
        $dispatcher = $this->container->get('event_dispatcher');
177
        $dispatcher->dispatch(BuildProcessComplete::NAME, new BuildProcessComplete());
178
    }
179
180
    public function watch()
181
    {
182
        $this->output->writeln('Building website...');
183
        $this->build(true);
184
        $this->output->writeln(sprintf('Watching %s', getcwd()));
185
186
        $exclusions = array_merge($this->getConfiguration()->getExcludes(), [
187
            $this->getConfiguration()->getTargetFolder(),
188
        ]);
189
        $fileExplorer = FileExplorer::create(
190
            getcwd(), $exclusions, $this->getConfiguration()->getIncludes()
191
        );
192
193
        $newWatcher = Watcher::create(getcwd());
194
        $newWatcher
195
            ->setLogger($this->output)
196
            ->setEventDispatcher($this->container->get('event_dispatcher'))
197
            ->setExcludePatterns(array_merge(
198
                $exclusions, FileExplorer::$vcsPatterns, [Configuration::CACHE_FOLDER]
199
            ))
200
            ->setIterator($fileExplorer->getExplorer())
201
//            ->addListener(Create::NAME, function ($e) { $this->watchListenerFunction($e); })
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
202
//            ->addListener(Modify::NAME, function ($e) { $this->watchListenerFunction($e); })
203
//            ->addListener(Move::NAME,   function ($e) { $this->watchListenerFunction($e); })
204
        ;
205
206
        $this->output->writeln('Watch started successfully');
207
208
        $newWatcher->start();
209
    }
210
211
    private function watchListenerFunction(AbstractEvent $event)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
212
    {
213
        $filePath = fs::getRelativePath($event->filename);
214
215
        try
216
        {
217
            switch ($event::getEventName())
218
            {
219
                case Create::NAME:
220
                    $this->creationWatcher($filePath);
221
                    break;
222
223
                case Modify::NAME:
224
                    $this->modificationWatcher($filePath);
225
                    break;
226
227
                case Move::NAME:
228
                    $newFile = fs::getRelativePath($event->destFilename);
0 ignored issues
show
Bug introduced by
The property destFilename does not seem to exist. Did you mean filename?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
229
230
                    $this->deletionWatcher($filePath);
0 ignored issues
show
Unused Code introduced by
The call to the method allejo\stakx\Website::deletionWatcher() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
231
                    $this->creationWatcher($newFile);
232
                    break;
233
            }
234
        }
235
        catch (FileAwareException $e)
236
        {
237
            $this->output->writeln(sprintf("Your website failed to build with the following error in file '%s': %s",
238
                $e->getPath(),
239
                $e->getMessage()
240
            ));
241
        }
242
        catch (\Exception $e)
243
        {
244
            $this->output->writeln(sprintf('Your website failed to build with the following error: %s',
245
                $e->getMessage()
246
            ));
247
        }
248
    }
249
250
    /**
251
     * @return Configuration
252
     */
253
    public function getConfiguration()
254
    {
255
        return $this->container->get(Configuration::class);
256
    }
257
258
    /**
259
     * Get whether or not the website is being built in Configuration-less mode.
260
     *
261
     * @return bool True when being built with no configuration file
262
     */
263
    public function isConfLess()
264
    {
265
        return $this->confLess;
266
    }
267
268
    /**
269
     * Set whether or not the website should be built with a configuration.
270
     *
271
     * @param bool $status True when a website should be built without a configuration
272
     */
273
    public function setConfLess($status)
274
    {
275
        $this->confLess = $status;
276
    }
277
278
    /**
279
     * @param string $filePath
280
     * @param mixed  $newlyCreate
281
     */
282
    private function creationWatcher($filePath, $newlyCreate = true)
283
    {
284
        if ($newlyCreate)
285
        {
286
            $this->output->writeln(sprintf('File creation detected: %s', $filePath));
287
        }
288
289
        if ($this->pm->shouldBeTracked($filePath))
290
        {
291
            try
292
            {
293
                $pageView = $this->pm->createNewItem($filePath);
294
295
                $this->compiler->compilePageView($pageView);
296
297
                unset($this->creationQueue[$filePath]);
298
            }
299
            catch (\Exception $e)
300
            {
301
                $this->creationQueue[$filePath] = true;
302
            }
303
        }
304
        elseif ($this->cm->shouldBeTracked($filePath))
305
        {
306
            try
307
            {
308
                $contentItem = $this->cm->createNewItem($filePath);
309
                TwigManager::getInstance()->addGlobal('collections', $this->cm->getCollections());
310
311
                $this->pm->trackNewContentItem($contentItem);
0 ignored issues
show
Compatibility introduced by
$contentItem of type object<allejo\stakx\Document\ReadableDocument> is not a sub-type of object<allejo\stakx\Document\ContentItem>. It seems like you assume a child class of the class allejo\stakx\Document\ReadableDocument 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...
312
                $this->compiler->compileContentItem($contentItem);
0 ignored issues
show
Deprecated Code introduced by
The method allejo\stakx\Compiler::compileContentItem() has been deprecated.

This method has been deprecated.

Loading history...
313
                $this->compiler->compileSome([
314
                    'namespace' => 'collections',
315
                    'dependency' => $contentItem->getNamespace(),
316
                ]);
317
318
                unset($this->creationQueue[$filePath]);
319
            }
320
            catch (\Exception $e)
321
            {
322
                $this->creationQueue[$filePath] = true;
323
            }
324
        }
325 View Code Duplication
        elseif ($this->dm->shouldBeTracked($filePath))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
326
        {
327
            $change = $this->dm->createNewItem($filePath);
328
            TwigManager::getInstance()->addGlobal('data', $this->dm->getDataItems());
329
330
            $this->compiler->compileSome([
331
                'namespace' => 'data',
332
                'dependency' => $change,
333
            ]);
334
        }
335
        elseif (!is_null($this->tm) && $this->tm->shouldBeTracked($filePath))
336
        {
337
            $this->tm->createNewItem($filePath);
338
        }
339
        elseif ($this->am->shouldBeTracked($filePath))
340
        {
341
            $this->am->createNewItem($filePath);
342
        }
343
    }
344
345
    /**
346
     * @param string $filePath
347
     */
348
    private function modificationWatcher($filePath)
349
    {
350
        $this->container->get('logger')->writeln(sprintf('File change detected: %s', $filePath));
351
352
        if (isset($this->creationQueue[$filePath]))
353
        {
354
            $this->creationWatcher($filePath, false);
355
        }
356
        elseif ($this->compiler->isParentTemplate($filePath))
357
        {
358
            TwigManager::getInstance()->clearTemplateCache();
0 ignored issues
show
Deprecated Code introduced by
The method Twig_Environment::clearTemplateCache() has been deprecated with message: since 1.18.3 (to be removed in 2.0)

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...
359
            $this->compiler->refreshParent($filePath);
360
        }
361
        elseif ($this->compiler->isImportDependency($filePath))
362
        {
363
            TwigManager::getInstance()->clearTemplateCache();
0 ignored issues
show
Deprecated Code introduced by
The method Twig_Environment::clearTemplateCache() has been deprecated with message: since 1.18.3 (to be removed in 2.0)

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...
364
            $this->compiler->compileImportDependencies($filePath);
365
        }
366
        elseif ($this->pm->isTracked($filePath))
367
        {
368
            $change = $this->pm->refreshItem($filePath);
369
370
            TwigManager::getInstance()->clearTemplateCache();
0 ignored issues
show
Deprecated Code introduced by
The method Twig_Environment::clearTemplateCache() has been deprecated with message: since 1.18.3 (to be removed in 2.0)

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...
371
            $this->compiler->compilePageView($change);
372
        }
373
        elseif ($this->cm->isTracked($filePath))
374
        {
375
            $contentItem = &$this->cm->getContentItem($filePath);
376
            $contentItem->readContent();
377
378
            $this->compiler->compileContentItem($contentItem);
0 ignored issues
show
Deprecated Code introduced by
The method allejo\stakx\Compiler::compileContentItem() has been deprecated.

This method has been deprecated.

Loading history...
379
            $this->compiler->compileSome([
380
                'namespace' => 'collections',
381
                'dependency' => $contentItem->getNamespace(),
382
            ]);
383
        }
384 View Code Duplication
        elseif ($this->dm->isTracked($filePath))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
385
        {
386
            $change = $this->dm->refreshItem($filePath);
387
            TwigManager::getInstance()->addGlobal('data', $this->dm->getDataItems());
388
389
            $this->compiler->compileSome([
390
                'namespace' => 'data',
391
                'dependency' => $change,
392
            ]);
393
        }
394
        elseif (!is_null($this->tm) && $this->tm->isTracked($filePath))
395
        {
396
            $this->tm->refreshItem($filePath);
397
        }
398
        elseif ($this->am->isTracked($filePath))
399
        {
400
            $this->am->refreshItem($filePath);
401
        }
402
    }
403
404
    /**
405
     * @param string $filePath
406
     */
407
    private function deletionWatcher($filePath)
0 ignored issues
show
Unused Code introduced by
The parameter $filePath is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
408
    {
409
    }
410
411
    /**
412
     * Prepare the Stakx environment by creating necessary cache folders.
413
     */
414
    private function createFolderStructure()
415
    {
416
        $targetDir = fs::absolutePath($this->getConfiguration()->getTargetFolder());
0 ignored issues
show
Documentation introduced by
$this->getConfiguration()->getTargetFolder() is of type string, but the function expects a object<string>.

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...
417
418
        if (!Service::getParameter(BuildableCommand::NO_CLEAN))
419
        {
420
            fs::remove($targetDir);
0 ignored issues
show
Bug introduced by
The method remove() does not exist on allejo\stakx\Filesystem\FilesystemLoader. Did you maybe mean removeExtension()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
421
        }
422
423
        if (!Service::getParameter(BuildableCommand::USE_CACHE))
424
        {
425
            fs::remove(fs::absolutePath(Configuration::CACHE_FOLDER, 'twig'));
0 ignored issues
show
Unused Code introduced by
The call to FilesystemLoader::absolutePath() has too many arguments starting with 'twig'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
Bug introduced by
The method remove() does not exist on allejo\stakx\Filesystem\FilesystemLoader. Did you maybe mean removeExtension()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
426
            fs::mkdir(fs::absolutePath(fs::appendPath(Configuration::CACHE_FOLDER, 'twig')));
427
        }
428
429
        fs::mkdir($targetDir);
430
    }
431
432
    /**
433
     * Configure the Highlighter object for highlighting code blocks.
434
     */
435
    private function configureHighlighter()
436
    {
437
        $enabled = Service::setParameter(Configuration::HIGHLIGHTER_ENABLED, $this->getConfiguration()->isHighlighterEnabled());
438
439
        if (!$enabled)
440
        {
441
            return;
442
        }
443
444
        foreach ($this->getConfiguration()->getHighlighterCustomLanguages() as $lang => $path)
445
        {
446
            $fullPath = fs::absolutePath($path);
447
448
            if (!fs::exists($fullPath))
449
            {
450
                $this->output->warning('The following language definition could not be found: {lang}', [
451
                    'lang' => $path,
452
                ]);
453
                continue;
454
            }
455
456
            Highlighter::registerLanguage($lang, $fullPath);
457
            $this->output->debug('Loading custom language {lang} from {path}...', [
458
                'lang' => $lang,
459
                'path' => $path,
460
            ]);
461
        }
462
    }
463
}
464