Passed
Push — develop ( 164850...0d721c )
by Andrew
04:24
created

Manifest::invalidateCaches()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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/
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
9
 * @copyright Copyright (c) 2018 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
10
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
11
12
namespace nystudio107\imageoptimize\helpers;
13
14
use Craft;
15
use craft\helpers\Json as JsonHelper;
16
use craft\helpers\UrlHelper;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, nystudio107\imageoptimize\helpers\UrlHelper. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
18
use yii\base\Exception;
19
use yii\caching\TagDependency;
20
use yii\web\NotFoundHttpException;
21
22
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
23
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
24
 * @package   Twigpack
25
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
26
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
27
class Manifest
28
{
29
    // Constants
30
    // =========================================================================
31
32
    const CACHE_KEY = 'twigpack';
33
    const CACHE_TAG = 'twigpack';
34
35
    const DEVMODE_CACHE_DURATION = 1;
36
37
    // Protected Static Properties
38
    // =========================================================================
39
40
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
41
     * @var array
42
     */
43
    protected static $files;
44
45
    // Public Static Methods
46
    // =========================================================================
47
48
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
49
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
50
     * @param string $moduleName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
51
     * @param bool   $async
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
52
     *
53
     * @return null|string
54
     * @throws NotFoundHttpException
55
     */
56
    public static function getCssModuleTags(array $config, string $moduleName, bool $async)
57
    {
58
        $legacyModule = self::getModule($config, $moduleName, 'legacy');
59
        if ($legacyModule === null) {
60
            return null;
61
        }
62
        $lines = [];
63
        if ($async) {
64
            $lines[] = "<link rel=\"preload\" href=\"{$legacyModule}\" as=\"style\" onload=\"this.rel='stylesheet'\" />";
65
            $lines[] = "<noscript><link rel=\"stylesheet\" href=\"{$legacyModule}\"></noscript>";
66
        } else {
67
            $lines[] = "<link rel=\"stylesheet\" href=\"{$legacyModule}\" />";
68
        }
69
70
        return implode("\r\n", $lines);
71
    }
72
73
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
74
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
75
     * @param string $moduleName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
76
     * @param bool   $async
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
77
     *
78
     * @return null|string
79
     * @throws NotFoundHttpException
80
     */
81
    public static function getJsModuleTags(array $config, string $moduleName, bool $async)
82
    {
83
        $legacyModule = self::getModule($config, $moduleName, 'legacy');
84
        if ($legacyModule === null) {
85
            return null;
86
        }
87
        if ($async) {
88
            $modernModule = self::getModule($config, $moduleName, 'modern');
89
            if ($modernModule === null) {
90
                return null;
91
            }
92
        }
93
        $lines = [];
94
        if ($async) {
95
            $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...
96
            $lines[] = "<script nomodule src=\"{$legacyModule}\"></script>";
97
        } else {
98
            $lines[] = "<script src=\"{$legacyModule}\"></script>";
99
        }
100
101
        return implode("\r\n", $lines);
102
    }
103
104
    /**
105
     * Safari 10.1 supports modules, but does not support the `nomodule`
106
     * attribute - it will load <script nomodule> anyway. This snippet solve
107
     * this problem, but only for script tags that load external code, e.g.:
108
     * <script nomodule src="nomodule.js"></script>
109
     *
110
     * Again: this will **not* # prevent inline script, e.g.:
111
     * <script nomodule>alert('no modules');</script>.
112
     *
113
     * This workaround is possible because Safari supports the non-standard
114
     * 'beforeload' event. This allows us to trap the module and nomodule load.
115
     *
116
     * Note also that `nomodule` is supported in later versions of Safari -
117
     * it's just 10.1 that omits this attribute.
118
     *
119
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
120
     *
121
     * @return string
122
     */
123
    public static function getSafariNomoduleFix(): string
124
    {
125
        return <<<EOT
126
<script>
127
!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()}}();
128
</script>
129
EOT;
130
    }
131
132
    /**
133
     * Return the URI to a module
134
     *
135
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
136
     * @param string $moduleName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
137
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
138
     *
139
     * @return null|string
140
     * @throws NotFoundHttpException
141
     */
142
    public static function getModule(array $config, string $moduleName, string $type = 'modern')
143
    {
144
        $module = null;
145
        // Determine whether we should use the devServer for HMR or not
146
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
147
        $isHot = ($devMode && $config['useDevServer']);
148
        // Get the manifest file
149
        $manifest = self::getManifestFile($config, $isHot, $type);
150
        if ($manifest !== null) {
151
            $module = $manifest[$moduleName];
152
            $prefix = $isHot
153
                ? $config['devServer']['publicPath']
154
                : $config['server']['publicPath'];
155
            // If the module isn't a full URL, prefix it
156
            if (!UrlHelper::isAbsoluteUrl($module)) {
157
                $module = self::combinePaths($prefix, $module);
158
            }
159
            // Make sure it's a full URL
160
            if (!UrlHelper::isAbsoluteUrl($module)) {
161
                try {
162
                    $module = UrlHelper::siteUrl($module);
163
                } catch (Exception $e) {
164
                    Craft::error($e->getMessage(), __METHOD__);
165
                }
166
            }
167
        }
168
169
        return $module;
170
    }
171
172
    /**
173
     * Return a JSON-decoded manifest file
174
     *
175
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
176
     * @param bool   $isHot
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
177
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
178
     *
179
     * @return null|array
180
     * @throws NotFoundHttpException
181
     */
182
    public static function getManifestFile(array $config, bool &$isHot, string $type = 'modern')
183
    {
184
        $manifest = null;
185
        // Try to get the manifest
186
        while ($manifest === null) {
187
            $manifestPath = $isHot
188
                ? $config['devServer']['manifestPath']
189
                : $config['server']['manifestPath'];
190
            // Normalize the path
191
            $path = self::combinePaths($manifestPath, $config['manifest'][$type]);
192
            $manifest = self::getJsonFileFromUri($path);
193
            // If the manifest isn't found, and it was hot, fall back on non-hot
194
            if ($manifest === null) {
195
                Craft::error(
196
                    Craft::t(
197
                        'image-optimize',
198
                        'Manifest file not found at: {manifestPath}',
199
                        ['manifestPath' => $manifestPath]
200
                    ),
201
                    __METHOD__
202
                );
203
                if ($isHot) {
204
                    // Try again, but not with home module replacement
205
                    $isHot = false;
206
                } else {
207
                    $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
208
                    if ($devMode) {
209
                        // We couldn't find a manifest; throw an error
210
                        throw new NotFoundHttpException(
211
                            Craft::t(
212
                                'image-optimize',
213
                                'Manifest file not found at: {manifestPath}',
214
                                ['manifestPath' => $manifestPath]
215
                            )
216
                        );
217
                    }
218
219
                    return null;
220
                }
221
            }
222
        }
223
224
        return $manifest;
225
    }
226
227
    /**
228
     * Invalidate all of the manifest caches
229
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
230
    public static function invalidateCaches()
231
    {
232
        $cache = Craft::$app->getCache();
233
        TagDependency::invalidate($cache, self::CACHE_TAG);
234
        Craft::info('All manifest caches cleared', __METHOD__);
235
    }
236
237
    // Protected Static Methods
238
    // =========================================================================
239
240
    /**
241
     * Return the contents of a file from a URI path
242
     *
243
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
244
     *
245
     * @return mixed
246
     */
247
    protected static function getJsonFileFromUri(string $path)
248
    {
249
        // Make sure it's a full URL
250
        if (!UrlHelper::isAbsoluteUrl($path) && !is_file($path)) {
251
            try {
252
                $path = UrlHelper::siteUrl($path);
253
            } catch (Exception $e) {
254
                Craft::error($e->getMessage(), __METHOD__);
255
            }
256
        }
257
258
        return self::getJsonFileContents($path);
259
    }
260
261
    /**
262
     * Return the contents of a file from the passed in path
263
     *
264
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
265
     *
266
     * @return mixed
267
     */
268
    protected static function getJsonFileContents(string $path)
269
    {
270
        // Return the memoized manifest if it exists
271
        if (!empty(self::$files[$path])) {
272
            return self::$files[$path];
273
        }
274
        // Create the dependency tags
275
        $dependency = new TagDependency([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
276
            'tags' => [
277
                self::CACHE_TAG,
278
                self::CACHE_TAG.$path,
279
            ],
280
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
281
        // Set the cache duration based on devMode
282
        $cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
283
            ? self::DEVMODE_CACHE_DURATION
284
            : null;
285
        // Get the result from the cache, or parse the file
286
        $cache = Craft::$app->getCache();
287
        $file = $cache->getOrSet(
288
            self::CACHE_KEY.$path,
289
            function () use ($path) {
290
                $result = null;
291
                $string = @file_get_contents($path);
292
                if ($string) {
293
                    $result = JsonHelper::decodeIfJson($string);
294
                }
295
296
                return $result;
297
            },
298
            $cacheDuration,
299
            $dependency
300
        );
301
        self::$files[$path] = $file;
302
303
        return $file;
304
    }
305
306
    /**
307
     * Combined the passed in paths, whether file system or URL
308
     *
309
     * @param string ...$paths
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
310
     *
311
     * @return string
312
     */
313
    protected static function combinePaths(string ...$paths): string
314
    {
315
        $last_key = \count($paths) - 1;
316
        array_walk($paths, function (&$val, $key) use ($last_key) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
317
            switch ($key) {
318
                case 0:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
319
                    $val = rtrim($val, '/ ');
320
                    break;
321
                case $last_key:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
322
                    $val = ltrim($val, '/ ');
323
                    break;
324
                default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
325
                    $val = trim($val, '/ ');
326
                    break;
327
            }
328
        });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
329
330
        $first = array_shift($paths);
331
        $last = array_pop($paths);
332
        $paths = array_filter($paths);
333
        array_unshift($paths, $first);
334
        $paths[] = $last;
335
336
        return implode('/', $paths);
337
    }
338
}
339