AssetManager::getJsLibUrls()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 4
nop 0
dl 0
loc 21
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * AssetManager.php
5
 *
6
 * Generate static files for Jaxon CSS and Javascript codes.
7
 *
8
 * @package jaxon-core
9
 * @author Thierry Feuzeu <[email protected]>
10
 * @copyright 2016 Thierry Feuzeu <[email protected]>
11
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
12
 * @link https://github.com/jaxon-php/jaxon-core
13
 */
14
15
namespace Jaxon\Plugin\Code;
16
17
use Jaxon\App\Config\ConfigManager;
18
use Jaxon\Config\Config;
19
use Jaxon\Plugin\AbstractPlugin;
20
use Jaxon\Plugin\CodeGeneratorInterface as Generator;
21
use Jaxon\Plugin\CssCodeGeneratorInterface as CssGenerator;
22
use Jaxon\Plugin\JsCodeGeneratorInterface as JsGenerator;
23
use Jaxon\Storage\StorageManager;
24
use Lagdo\Facades\Logger;
25
use League\Flysystem\Filesystem;
26
use Closure;
27
use Throwable;
28
29
use function implode;
30
use function is_array;
31
use function is_string;
32
use function is_subclass_of;
33
use function rtrim;
34
use function trim;
35
36
class AssetManager
37
{
38
    /**
39
     * @var array<Filesystem>
40
     */
41
    protected array $aStorage = [];
42
43
    /**
44
     * Default library URL
45
     *
46
     * @var string
47
     */
48
    private const JS_LIB_URL = 'https://cdn.jsdelivr.net/gh/jaxon-php/[email protected]/dist';
49
50
    /**
51
     * The constructor
52
     *
53
     * @param ConfigManager $xConfigManager
54
     * @param StorageManager $xStorageManager
55
     * @param MinifierInterface $xMinifier
56
     */
57
    public function __construct(private ConfigManager $xConfigManager,
58
        private StorageManager $xStorageManager, private MinifierInterface $xMinifier)
59
    {}
60
61
    /**
62
     * @return Config
63
     */
64
    protected function config(): Config
65
    {
66
        return $this->xConfigManager->getExportConfig();
67
    }
68
69
    /**
70
     * @param string $sAsset "js" or "css"
71
     *
72
     * @return Filesystem
73
     */
74
    protected function _storage(string $sAsset): Filesystem
75
    {
76
        if($this->config()->hasOption('storage'))
77
        {
78
            return $this->xStorageManager->get($this->config()->getOption('storage'));
79
        }
80
81
        $sRootDir = $this->getAssetDir($sAsset);
82
        // Fylsystem options: we don't want the root dir to be created if it doesn't exist.
83
        $aAdapterOptions = ['lazyRootCreation' => true];
84
        $aDirOptions = [
85
            'config' => [
86
                'public_url' => $this->getAssetUri($sAsset),
87
            ],
88
        ];
89
        return $this->xStorageManager
90
            ->adapter('local', $aAdapterOptions)
91
            ->make($sRootDir, $aDirOptions);
92
    }
93
94
    /**
95
     * @param string $sAsset "js" or "css"
96
     *
97
     * @return Filesystem
98
     */
99
    protected function storage(string $sAsset): Filesystem
100
    {
101
        return $this->aStorage[$sAsset] ??= $this->_storage($sAsset);
102
    }
103
104
    /**
105
     * @param array $aValues
106
     *
107
     * @return string
108
     */
109
    public function makeFileOptions(array $aValues): string
110
    {
111
        if(!isset($aValues['options']) || !$aValues['options'])
112
        {
113
            return '';
114
        }
115
        if(is_array($aValues['options']))
116
        {
117
            $aOptions = [];
118
            foreach($aValues['options'] as $sName => $sValue)
119
            {
120
                $aOptions[] = "{$sName}=\"" . trim($sValue) . '"';
121
            }
122
            return implode(' ', $aOptions);
123
        }
124
        if(is_string($aValues['options']))
125
        {
126
            return trim($aValues['options']);
127
        }
128
        return '';
129
    }
130
131
    /**
132
     * Get app js options
133
     *
134
     * @return string
135
     */
136
    public function getJsOptions(): string
137
    {
138
        // Revert to the options in the "lib" section in the config,
139
        // if there is no options defined in the 'app' section.
140
        if(!$this->xConfigManager->hasAppOption('assets'))
141
        {
142
            $sOptions = trim($this->config()->getOption('js.options', ''));
143
            return $sOptions === '' ? 'charset="UTF-8"' : "$sOptions charset=\"UTF-8\"";
144
        }
145
146
        return $this->makeFileOptions([
147
            'options' => $this->config()->getOption('js.options', ''),
148
        ]);
149
    }
150
151
    /**
152
     * Get app js options
153
     *
154
     * @return string
155
     */
156
    public function getCssOptions(): string
157
    {
158
        return $this->makeFileOptions([
159
            'options' => $this->config()->getOption('css.options', ''),
160
        ]);
161
    }
162
163
    /**
164
     * Check if the assets of this plugin shall be included in Jaxon generated code.
165
     *
166
     * @param Generator|CssGenerator|JsGenerator $xGenerator
167
     *
168
     * @return bool
169
     */
170
    public function shallIncludeAssets(Generator|CssGenerator|JsGenerator $xGenerator): bool
171
    {
172
        if(!is_subclass_of($xGenerator, AbstractPlugin::class))
173
        {
174
            return true;
175
        }
176
177
        /** @var AbstractPlugin */
178
        $xPlugin = $xGenerator;
179
        $sPluginOptionName = 'include.' . $xPlugin->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Jaxon\Plugin\CodeGeneratorInterface. It seems like you code against a sub-type of Jaxon\Plugin\CodeGeneratorInterface such as Jaxon\Plugin\AbstractPlugin. ( Ignorable by Annotation )

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

179
        $sPluginOptionName = 'include.' . $xPlugin->/** @scrutinizer ignore-call */ getName();
Loading history...
Bug introduced by
The method getName() does not exist on Jaxon\Plugin\CssCodeGeneratorInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Jaxon\Plugin\CssCodeGeneratorInterface. ( Ignorable by Annotation )

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

179
        $sPluginOptionName = 'include.' . $xPlugin->/** @scrutinizer ignore-call */ getName();
Loading history...
Bug introduced by
The method getName() does not exist on Jaxon\Plugin\JsCodeGeneratorInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Jaxon\Plugin\Code\ConfigScriptGenerator or Jaxon\Plugin\Code\ReadyScriptGenerator. Are you sure you never get one of those? ( Ignorable by Annotation )

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

179
        $sPluginOptionName = 'include.' . $xPlugin->/** @scrutinizer ignore-call */ getName();
Loading history...
180
181
        return $this->config()->hasOption($sPluginOptionName) ?
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->config()->...on('include.all', true) could return the type array|null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
182
            $this->config()->getOption($sPluginOptionName) :
183
            $this->config()->getOption('include.all', true);
184
    }
185
186
    /**
187
     * Get the HTML tags to include Jaxon javascript files into the page
188
     *
189
     * @return array
190
     */
191
    public function getJsLibUrls(): array
192
    {
193
        $sJsExtension = $this->config()->getOption('minify') ? '.min.js' : '.js';
194
        // The URI for the javascript library files
195
        $sJsLibUri = $this->xConfigManager->getOption('js.lib.uri', self::JS_LIB_URL);
196
        $sJsLibUri = rtrim($sJsLibUri, '/');
197
198
        // Add component files to the javascript file array.
199
        $sChibiUrl = "$sJsLibUri/libs/chibi/chibi$sJsExtension";
200
        $aJsUrls = [
201
            $this->xConfigManager->getOption('js.lib.jq', $sChibiUrl),
202
            "$sJsLibUri/jaxon.core$sJsExtension",
203
        ];
204
        if($this->xConfigManager->getOption('core.debug.on'))
205
        {
206
            $sLanguage = $this->xConfigManager->getOption('core.language');
207
            $aJsUrls[] = "$sJsLibUri/jaxon.debug$sJsExtension";
208
            $aJsUrls[] = "$sJsLibUri/lang/jaxon.$sLanguage$sJsExtension";
209
        }
210
211
        return $aJsUrls;
212
    }
213
214
    /**
215
     * @param string $sAsset "js" or "css"
216
     *
217
     * @return string
218
     */
219
    private function getAssetUri(string $sAsset): string
220
    {
221
        return rtrim($this->config()->hasOption("$sAsset.uri") ?
0 ignored issues
show
Bug introduced by
It seems like $this->config()->hasOpti...)->getOption('uri', '') can also be of type array and null; however, parameter $string of rtrim() 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

221
        return rtrim(/** @scrutinizer ignore-type */ $this->config()->hasOption("$sAsset.uri") ?
Loading history...
222
            $this->config()->getOption("$sAsset.uri") :
223
            $this->config()->getOption('uri', ''), '/');
224
    }
225
226
    /**
227
     * @param string $sAsset "js" or "css"
228
     *
229
     * @return string
230
     */
231
    private function getAssetDir(string $sAsset): string
232
    {
233
        return rtrim($this->config()->hasOption("$sAsset.dir") ?
0 ignored issues
show
Bug introduced by
It seems like $this->config()->hasOpti...)->getOption('dir', '') can also be of type array and null; however, parameter $string of rtrim() 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

233
        return rtrim(/** @scrutinizer ignore-type */ $this->config()->hasOption("$sAsset.dir") ?
Loading history...
234
            $this->config()->getOption("$sAsset.dir") :
235
            $this->config()->getOption('dir', ''), '/\/');
236
    }
237
238
    /**
239
     * @param Closure $cGetHash
240
     * @param string $sAsset "js" or "css"
241
     *
242
     * @return string
243
     */
244
    private function getAssetFile(Closure $cGetHash, string $sAsset): string
245
    {
246
        return $this->config()->hasOption("$sAsset.file") ?
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->config()->...n('file') : $cGetHash() could return the type array|null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
247
            $this->config()->getOption("$sAsset.file") :
248
            // Make sure to call invoke callback only if required.
249
            ($this->config()->hasOption("file") ?
250
            $this->config()->getOption("file") : $cGetHash());
251
    }
252
253
    /**
254
     * @param string $sAsset "js" or "css"
255
     *
256
     * @return bool
257
     */
258
    private function minifyEnabled(string $sAsset): bool
259
    {
260
        return $this->config()->hasOption("$sAsset.minify") ?
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->config()->...Option('minify', false) could return the type array|null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
261
            $this->config()->getOption("$sAsset.minify") :
262
            $this->config()->getOption('minify', false);
263
    }
264
265
    /**
266
     * @param string $sAsset "js" or "css"
267
     *
268
     * @return bool
269
     */
270
    private function exportEnabled(string $sAsset): bool
271
    {
272
        return $this->config()->hasOption("$sAsset.export") ?
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->config()->...Option('export', false) could return the type array|null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
273
            $this->config()->getOption("$sAsset.export") :
274
            $this->config()->getOption('export', false);
275
    }
276
277
    /**
278
     * @param Filesystem $xStorage
279
     * @param string $sFilePath
280
     *
281
     * @return bool
282
     */
283
    private function fileExists(Filesystem $xStorage, string $sFilePath): bool
284
    {
285
        try
286
        {
287
            return $xStorage->fileExists($sFilePath);
288
        }
289
        catch(Throwable $e)
290
        {
291
            Logger::warning("Unable to check asset file at $sFilePath.", [
292
                'error' => $e->getMessage(),
293
            ]);
294
            return false;
295
        }
296
    }
297
298
    /**
299
     * @param Filesystem $xStorage
300
     * @param string $sFilePath
301
     * @param string $sContent
302
     *
303
     * @return bool
304
     */
305
    private function writeFile(Filesystem $xStorage, string $sFilePath, string $sContent): bool
306
    {
307
        try
308
        {
309
            $xStorage->write($sFilePath, $sContent);
310
            return true;
311
        }
312
        catch(Throwable $e)
313
        {
314
            Logger::warning("Unable to write to asset file at $sFilePath.", [
315
                'error' => $e->getMessage(),
316
            ]);
317
            return false;
318
        }
319
    }
320
321
    /**
322
     * @param string $sAsset "js" or "css"
323
     * @param string $sFilePath
324
     * @param string $sMinFilePath
325
     *
326
     * @return bool
327
     */
328
    private function minifyAsset(string $sAsset, string $sFilePath, string $sMinFilePath): bool
329
    {
330
        if(!$this->minifyEnabled($sAsset))
331
        {
332
            return false;
333
        }
334
335
        $xStorage = $this->storage($sAsset);
336
        if($xStorage->fileExists($sMinFilePath))
337
        {
338
            return true;
339
        }
340
341
        $sMinContent = $sAsset === 'js' ?
342
            $this->xMinifier->minifyJsCode($xStorage->read($sFilePath)) :
343
            $this->xMinifier->minifyCssCode($xStorage->read($sFilePath));
344
        if($sMinContent === false || $sMinContent === '')
345
        {
346
            return false;
347
        }
348
349
        return $this->writeFile($xStorage, $sMinFilePath, $sMinContent);
350
    }
351
352
    private function getPublicUrl(string $sFilePath, string $sAsset): string
353
    {
354
        $sUri = $this->getAssetUri($sAsset);
355
        return $sUri !== '' ? "$sUri/$sFilePath" :
356
            $this->storage($sAsset)->publicUrl($sFilePath);
357
    }
358
359
    /**
360
     * Write javascript or css files and return the corresponding URI
361
     *
362
     * @param Closure $cGetHash
363
     * @param Closure $cGetCode
364
     * @param string $sAsset "js" or "css"
365
     *
366
     * @return string
367
     */
368
    public function createFiles(Closure $cGetHash, Closure $cGetCode, string $sAsset): string
369
    {
370
        // Check if the config options allow the file creation.
371
        // - The assets.js.export option must be set to true
372
        // - The assets.js.uri and assets.js.dir options must be set to non null values
373
        if(!$this->exportEnabled($sAsset) ||
374
            // $this->getAssetUri($sAsset) === '' ||
375
            $this->getAssetDir($sAsset) === '')
376
        {
377
            return '';
378
        }
379
380
        // Check dir access
381
        $xStorage = $this->storage($sAsset);
382
        $sFileName = $this->getAssetFile($cGetHash, $sAsset);
383
        // - The assets.js.dir must be writable
384
        if(!$sFileName || !$xStorage->directoryExists('') /*|| $xStorage->visibility('') !== 'public'*/)
385
        {
386
            return '';
387
        }
388
389
        $sFilePath = "{$sFileName}.{$sAsset}";
390
        $sMinFilePath = "{$sFileName}.min.{$sAsset}";
391
392
        // Try to create the file and write the code, if it doesn't exist.
393
        if(!$this->fileExists($xStorage, $sFilePath) &&
394
            !$this->writeFile($xStorage, $sFilePath, $cGetCode()))
395
        {
396
            return '';
397
        }
398
399
        if(!$this->minifyEnabled($sAsset))
400
        {
401
            return $this->getPublicUrl($sFilePath, $sAsset);
402
        }
403
404
        // If the file cannot be minified, return the plain js file.
405
        return $this->minifyAsset($sAsset, $sFilePath, $sMinFilePath) ?
406
            $this->getPublicUrl($sMinFilePath, $sAsset) :
407
            $this->getPublicUrl($sFilePath, $sAsset);
408
    }
409
410
    /**
411
     * Write javascript files and return the corresponding URI
412
     *
413
     * @param Closure $cGetHash
414
     * @param Closure $cGetCode
415
     *
416
     * @return string
417
     */
418
    public function createJsFiles(Closure $cGetHash, Closure $cGetCode): string
419
    {
420
        // Using closures, so the code generator is actually called only if it is really required.
421
        return $this->createFiles($cGetHash, $cGetCode, 'js');
422
    }
423
424
    /**
425
     * Write javascript files and return the corresponding URI
426
     *
427
     * @param Closure $cGetHash
428
     * @param Closure $cGetCode
429
     *
430
     * @return string
431
     */
432
    public function createCssFiles(Closure $cGetHash, Closure $cGetCode): string
433
    {
434
        // Using closures, so the code generator is actually called only if it is really required.
435
        return $this->createFiles($cGetHash, $cGetCode, 'css');
436
    }
437
}
438