Passed
Push — develop ( 73258c...5d53d7 )
by Andrew
03:10
created

Manifest   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 168
dl 0
loc 461
rs 7.44
c 0
b 0
f 0
wmc 52

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getFileContents() 0 39 5
A getCriticalCssTags() 0 21 3
A getJsonFile() 0 3 1
B getModule() 0 28 8
A getCssRelPreloadPolyfill() 0 3 1
A reportError() 0 7 3
A getModuleEntry() 0 20 3
A getFileFromUri() 0 17 5
A invalidateCaches() 0 5 1
A jsonFileDecode() 0 3 1
A getJsModuleTags() 0 21 5
A getCssModuleTags() 0 15 3
A combinePaths() 0 24 3
A getSafariNomoduleFix() 0 3 1
A getFile() 0 3 1
A getCssInlineTags() 0 9 2
B getManifestFile() 0 33 6

How to fix   Complexity   

Complex Class

Complex classes like Manifest 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.

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 Manifest, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Twigpack plugin for Craft CMS 3.x
4
 *
5
 * Twigpack is the conduit between Twig and webpack, with manifest.json &
6
 * webpack-dev-server HMR support
7
 *
8
 * @link      https://nystudio107.com/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
11
12
namespace nystudio107\twigpack\helpers;
13
14
use Craft;
15
use craft\helpers\Json as JsonHelper;
16
use craft\helpers\UrlHelper;
17
18
use nystudio107\twigpack\Twigpack;
19
use yii\base\Exception;
20
use yii\caching\TagDependency;
21
use yii\web\NotFoundHttpException;
22
23
/**
24
 * @author    nystudio107
25
 * @package   Twigpack
26
 * @since     1.0.0
27
 */
28
class Manifest
29
{
30
    // Constants
31
    // =========================================================================
32
33
    const CACHE_KEY = 'twigpack';
34
    const CACHE_TAG = 'twigpack';
35
36
    const DEVMODE_CACHE_DURATION = 1;
37
38
    // Protected Static Properties
39
    // =========================================================================
40
41
    /**
42
     * @var array
43
     */
44
    protected static $files;
45
46
    /**
47
     * @var bool
48
     */
49
    protected static $isHot = false;
50
51
    // Public Static Methods
52
    // =========================================================================
53
54
    /**
55
     * @param array  $config
56
     * @param string $moduleName
57
     * @param bool   $async
58
     *
59
     * @return string
60
     * @throws NotFoundHttpException
61
     */
62
    public static function getCssModuleTags(array $config, string $moduleName, bool $async): string
63
    {
64
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
65
        if ($legacyModule === null) {
66
            return '';
67
        }
68
        $lines = [];
69
        if ($async) {
70
            $lines[] = "<link rel=\"preload\" href=\"{$legacyModule}\" as=\"style\" onload=\"this.onload=null;this.rel='stylesheet'\" />";
71
            $lines[] = "<noscript><link rel=\"stylesheet\" href=\"{$legacyModule}\"></noscript>";
72
        } else {
73
            $lines[] = "<link rel=\"stylesheet\" href=\"{$legacyModule}\" />";
74
        }
75
76
        return implode("\r\n", $lines);
77
    }
78
79
    /**
80
     * @param string $path
81
     *
82
     * @return string
83
     */
84
    public static function getCssInlineTags(string $path): string
85
    {
86
        $result = self::getFile($path);
87
        if ($result) {
88
            $result = "<style>\r\n".$result."</style>\r\n";
89
            return $result;
90
        }
91
92
        return '';
93
    }
94
95
    /**
96
     * @param array       $config
97
     * @param null|string $name
98
     *
99
     * @return string
100
     */
101
    public static function getCriticalCssTags(array $config, $name = null): string
102
    {
103
        // Resolve the template name
104
        $template = Craft::$app->getView()->resolveTemplate($name ?? Twigpack::$templateName);
105
        if ($template) {
106
            $name = self::combinePaths(
107
                pathinfo($template, PATHINFO_DIRNAME),
108
                pathinfo($template, PATHINFO_FILENAME)
109
            );
110
            $dirPrefix = 'templates/';
111
            if (\defined('CRAFT_TEMPLATES_PATH')) {
112
                $dirPrefix = CRAFT_TEMPLATES_PATH;
113
            }
114
            $name = strstr($name, $dirPrefix);
115
            $name = str_replace($dirPrefix, '', $name);
116
            $path = self::combinePaths($config['critical']['basePath'], $name).$config['critical']['suffix'];
117
118
            return self::getCssInlineTags($path);
119
        }
120
121
        return '';
122
    }
123
124
    /**
125
     * Returns the uglified loadCSS rel=preload Polyfill as per:
126
     * https://github.com/filamentgroup/loadCSS#how-to-use-loadcss-recommended-example
127
     *
128
     * @return string
129
     */
130
    public static function getCssRelPreloadPolyfill(): string
131
    {
132
        return <<<EOT
133
<script>
134
/*! loadCSS. [c]2017 Filament Group, Inc. MIT License */
135
!function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={};if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){var e=t.media||"all";function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},e.poly=function(){if(!e.support())for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel||"style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")||(o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500);t.addEventListener?t.addEventListener("load",function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload",function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS}("undefined"!=typeof global?global:this);
136
</script>
137
EOT;
138
    }
139
140
    /**
141
     * @param array  $config
142
     * @param string $moduleName
143
     * @param bool   $async
144
     *
145
     * @return null|string
146
     * @throws NotFoundHttpException
147
     */
148
    public static function getJsModuleTags(array $config, string $moduleName, bool $async)
149
    {
150
        $legacyModule = self::getModule($config, $moduleName, 'legacy');
151
        if ($legacyModule === null) {
152
            return '';
153
        }
154
        if ($async) {
155
            $modernModule = self::getModule($config, $moduleName, 'modern');
156
            if ($modernModule === null) {
157
                return '';
158
            }
159
        }
160
        $lines = [];
161
        if ($async) {
162
            $lines[] = "<script type=\"module\" src=\"{$modernModule}\"></script>";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $modernModule does not seem to be defined for all execution paths leading up to this point.
Loading history...
163
            $lines[] = "<script nomodule src=\"{$legacyModule}\"></script>";
164
        } else {
165
            $lines[] = "<script src=\"{$legacyModule}\"></script>";
166
        }
167
168
        return implode("\r\n", $lines);
169
    }
170
171
    /**
172
     * Safari 10.1 supports modules, but does not support the `nomodule`
173
     * attribute - it will load <script nomodule> anyway. This snippet solve
174
     * this problem, but only for script tags that load external code, e.g.:
175
     * <script nomodule src="nomodule.js"></script>
176
     *
177
     * Again: this will **not* # prevent inline script, e.g.:
178
     * <script nomodule>alert('no modules');</script>.
179
     *
180
     * This workaround is possible because Safari supports the non-standard
181
     * 'beforeload' event. This allows us to trap the module and nomodule load.
182
     *
183
     * Note also that `nomodule` is supported in later versions of Safari -
184
     * it's just 10.1 that omits this attribute.
185
     *
186
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
187
     *
188
     * @return string
189
     */
190
    public static function getSafariNomoduleFix(): string
191
    {
192
        return <<<EOT
193
<script>
194
!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
195
</script>
196
EOT;
197
    }
198
199
    /**
200
     * Return the URI to a module
201
     *
202
     * @param array  $config
203
     * @param string $moduleName
204
     * @param string $type
205
     * @param bool   $soft
206
     *
207
     * @return null|string
208
     * @throws NotFoundHttpException
209
     */
210
    public static function getModule(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
211
    {
212
        // Get the module entry
213
        $module = self::getModuleEntry($config, $moduleName, $type, $soft);
214
        if ($module !== null) {
215
            $prefix = self::$isHot
216
                ? $config['devServer']['publicPath']
217
                : $config['server']['publicPath'];
218
            // If the module isn't a full URL, prefix it
219
            if (!UrlHelper::isAbsoluteUrl($module)) {
220
                $module = self::combinePaths($prefix, $module);
221
            }
222
            // Resolve any aliases
223
            $alias = Craft::getAlias($module, false);
224
            if ($alias) {
225
                $module = $alias;
226
            }
227
            // Make sure it's a full URL
228
            if (!UrlHelper::isAbsoluteUrl($module) && !is_file($module)) {
0 ignored issues
show
Bug introduced by
It seems like $module can also be of type true; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

228
            if (!UrlHelper::isAbsoluteUrl($module) && !is_file(/** @scrutinizer ignore-type */ $module)) {
Loading history...
Bug introduced by
It seems like $module can also be of type true; however, parameter $url of craft\helpers\UrlHelper::isAbsoluteUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

228
            if (!UrlHelper::isAbsoluteUrl(/** @scrutinizer ignore-type */ $module) && !is_file($module)) {
Loading history...
229
                try {
230
                    $module = UrlHelper::siteUrl($module);
0 ignored issues
show
Bug introduced by
It seems like $module can also be of type true; however, parameter $path of craft\helpers\UrlHelper::siteUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
                    $module = UrlHelper::siteUrl(/** @scrutinizer ignore-type */ $module);
Loading history...
231
                } catch (Exception $e) {
232
                    Craft::error($e->getMessage(), __METHOD__);
233
                }
234
            }
235
        }
236
237
        return $module;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $module also could return the type true which is incompatible with the documented return type null|string.
Loading history...
238
    }
239
240
    /**
241
     * Return a module's raw entry from the manifest
242
     *
243
     * @param array  $config
244
     * @param string $moduleName
245
     * @param string $type
246
     * @param bool   $soft
247
     *
248
     * @return null|string
249
     * @throws NotFoundHttpException
250
     */
251
    public static function getModuleEntry(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
252
    {
253
        $module = null;
254
        // Get the manifest file
255
        $manifest = self::getManifestFile($config, $type);
256
        if ($manifest !== null) {
257
            // Make sure it exists in the manifest
258
            if (empty($manifest[$moduleName])) {
259
                self::reportError(Craft::t(
260
                    'twigpack',
261
                    'Module does not exist in the manifest: {moduleName}',
262
                    ['moduleName' => $moduleName]
263
                ), $soft);
264
265
                return null;
266
            }
267
            $module = $manifest[$moduleName];
268
        }
269
270
        return $module;
271
    }
272
273
    /**
274
     * Return a JSON-decoded manifest file
275
     *
276
     * @param array  $config
277
     * @param string $type
278
     *
279
     * @return null|array
280
     * @throws NotFoundHttpException
281
     */
282
    public static function getManifestFile(array $config, string $type = 'modern')
283
    {
284
        $manifest = null;
285
        // Determine whether we should use the devServer for HMR or not
286
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
287
        self::$isHot = ($devMode && $config['useDevServer']);
288
        // Try to get the manifest
289
        while ($manifest === null) {
290
            $manifestPath = self::$isHot
291
                ? $config['devServer']['manifestPath']
292
                : $config['server']['manifestPath'];
293
            // Normalize the path
294
            $path = self::combinePaths($manifestPath, $config['manifest'][$type]);
295
            $manifest = self::getJsonFile($path);
296
            // If the manifest isn't found, and it was hot, fall back on non-hot
297
            if ($manifest === null) {
298
                // We couldn't find a manifest; throw an error
299
                self::reportError(Craft::t(
300
                    'twigpack',
301
                    'Manifest file not found at: {manifestPath}',
302
                    ['manifestPath' => $manifestPath]
303
                ), true);
304
                if (self::$isHot) {
305
                    // Try again, but not with home module replacement
306
                    self::$isHot = false;
307
                } else {
308
                    // Give up and return null
309
                    return null;
310
                }
311
            }
312
        }
313
314
        return $manifest;
315
    }
316
317
    /**
318
     * Returns the contents of a file from a URI path
319
     *
320
     * @param string $path
321
     *
322
     * @return string
323
     */
324
    public static function getFile(string $path): string
325
    {
326
        return self::getFileFromUri($path, null) ?? '';
327
    }
328
329
    /**
330
     * Return the contents of a JSON file from a URI path
331
     *
332
     * @param string $path
333
     *
334
     * @return null|array
335
     */
336
    protected static function getJsonFile(string $path)
337
    {
338
        return self::getFileFromUri($path, [self::class, 'jsonFileDecode']);
339
    }
340
341
    /**
342
     * Invalidate all of the manifest caches
343
     */
344
    public static function invalidateCaches()
345
    {
346
        $cache = Craft::$app->getCache();
347
        TagDependency::invalidate($cache, self::CACHE_TAG);
348
        Craft::info('All manifest caches cleared', __METHOD__);
349
    }
350
351
    // Protected Static Methods
352
    // =========================================================================
353
354
    /**
355
     * Return the contents of a file from a URI path
356
     *
357
     * @param string        $path
358
     * @param callable|null $callback
359
     *
360
     * @return null|mixed
361
     */
362
    protected static function getFileFromUri(string $path, callable $callback = null)
363
    {
364
        // Resolve any aliases
365
        $alias = Craft::getAlias($path, false);
366
        if ($alias) {
367
            $path = $alias;
368
        }
369
        // Make sure it's a full URL
370
        if (!UrlHelper::isAbsoluteUrl($path) && !is_file($path)) {
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type true; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

370
        if (!UrlHelper::isAbsoluteUrl($path) && !is_file(/** @scrutinizer ignore-type */ $path)) {
Loading history...
Bug introduced by
It seems like $path can also be of type true; however, parameter $url of craft\helpers\UrlHelper::isAbsoluteUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

370
        if (!UrlHelper::isAbsoluteUrl(/** @scrutinizer ignore-type */ $path) && !is_file($path)) {
Loading history...
371
            try {
372
                $path = UrlHelper::siteUrl($path);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type true; however, parameter $path of craft\helpers\UrlHelper::siteUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

372
                $path = UrlHelper::siteUrl(/** @scrutinizer ignore-type */ $path);
Loading history...
373
            } catch (Exception $e) {
374
                Craft::error($e->getMessage(), __METHOD__);
375
            }
376
        }
377
378
        return self::getFileContents($path, $callback);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type true; however, parameter $path of nystudio107\twigpack\hel...fest::getFileContents() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

378
        return self::getFileContents(/** @scrutinizer ignore-type */ $path, $callback);
Loading history...
379
    }
380
381
    /**
382
     * Return the contents of a file from the passed in path
383
     *
384
     * @param string   $path
385
     * @param callable $callback
386
     *
387
     * @return null|mixed
388
     */
389
    protected static function getFileContents(string $path, callable $callback = null)
390
    {
391
        // Return the memoized manifest if it exists
392
        if (!empty(self::$files[$path])) {
393
            return self::$files[$path];
394
        }
395
        // Create the dependency tags
396
        $dependency = new TagDependency([
397
            'tags' => [
398
                self::CACHE_TAG,
399
                self::CACHE_TAG.$path,
400
            ],
401
        ]);
402
        // Set the cache duration based on devMode
403
        $cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
404
            ? self::DEVMODE_CACHE_DURATION
405
            : null;
406
        // Get the result from the cache, or parse the file
407
        $cache = Craft::$app->getCache();
408
        $file = $cache->getOrSet(
409
            self::CACHE_KEY.$path,
410
            function () use ($path, $callback) {
411
                $result = null;
412
                $contents = @file_get_contents($path);
413
                if ($contents) {
414
                    $result = $contents;
415
                    if ($callback) {
416
                        $result = $callback($result);
417
                    }
418
                }
419
420
                return $result;
421
            },
422
            $cacheDuration,
423
            $dependency
424
        );
425
        self::$files[$path] = $file;
426
427
        return $file;
428
    }
429
430
    /**
431
     * Combined the passed in paths, whether file system or URL
432
     *
433
     * @param string ...$paths
434
     *
435
     * @return string
436
     */
437
    protected static function combinePaths(string ...$paths): string
438
    {
439
        $last_key = \count($paths) - 1;
440
        array_walk($paths, function (&$val, $key) use ($last_key) {
441
            switch ($key) {
442
                case 0:
443
                    $val = rtrim($val, '/ ');
444
                    break;
445
                case $last_key:
446
                    $val = ltrim($val, '/ ');
447
                    break;
448
                default:
449
                    $val = trim($val, '/ ');
450
                    break;
451
            }
452
        });
453
454
        $first = array_shift($paths);
455
        $last = array_pop($paths);
456
        $paths = array_filter($paths);
457
        array_unshift($paths, $first);
458
        $paths[] = $last;
459
460
        return implode('/', $paths);
461
    }
462
463
    /**
464
     * @param string $error
465
     * @param bool   $soft
466
     *
467
     * @throws NotFoundHttpException
468
     */
469
    protected static function reportError(string $error, $soft = false)
470
    {
471
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
472
        if ($devMode && !$soft) {
473
            throw new NotFoundHttpException($error);
474
        }
475
        Craft::error($error, __METHOD__);
476
    }
477
478
    // Private Static Methods
479
    // =========================================================================
480
481
    /**
482
     * @param $string
483
     *
484
     * @return mixed
485
     */
486
    private static function jsonFileDecode($string)
487
    {
488
        return JsonHelper::decodeIfJson($string);
489
    }
490
}
491