Passed
Push — main ( 82104f...ee6fb2 )
by Thierry
05:16
created

AssetManager::getJsOptions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 12
rs 10
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\Config\ConfigSetter;
20
use Jaxon\Plugin\AbstractPlugin;
21
use Jaxon\Plugin\CodeGeneratorInterface as Generator;
22
use Jaxon\Plugin\CssCodeGeneratorInterface as CssGenerator;
23
use Jaxon\Plugin\JsCodeGeneratorInterface as JsGenerator;
24
use Jaxon\Storage\StorageManager;
25
use Lagdo\Facades\Logger;
26
use League\Flysystem\Filesystem;
27
use Closure;
28
use Throwable;
29
30
use function implode;
31
use function is_array;
32
use function is_string;
33
use function is_subclass_of;
34
use function rtrim;
35
use function trim;
36
37
class AssetManager
38
{
39
    /**
40
     * @var Config|null
41
     */
42
    protected Config|null $xConfig = null;
43
44
    /**
45
     * @var array<Filesystem>
46
     */
47
    protected array $aStorage = [];
48
49
    /**
50
     * Default library URL
51
     *
52
     * @var string
53
     */
54
    private const JS_LIB_URL = 'https://cdn.jsdelivr.net/gh/jaxon-php/[email protected]/dist';
55
56
    /**
57
     * The constructor
58
     *
59
     * @param ConfigManager $xConfigManager
60
     * @param StorageManager $xStorageManager
61
     * @param MinifierInterface $xMinifier
62
     */
63
    public function __construct(private ConfigManager $xConfigManager,
64
        private StorageManager $xStorageManager, private MinifierInterface $xMinifier)
65
    {}
66
67
    /**
68
     * @return Config
69
     */
70
    protected function config(): Config
71
    {
72
        if($this->xConfig !== null)
73
        {
74
            return $this->xConfig;
75
        }
76
77
        $xConfigSetter = new ConfigSetter();
78
        // Copy the assets options in a new config object.
79
        return $this->xConfig = $this->xConfigManager->hasAppOption('assets') ?
80
            $xConfigSetter->newConfig($this->xConfigManager->getAppOption('assets')) :
81
            // Convert the options in the "lib" section to the same format as in the "app" section.
82
            $xConfigSetter->newConfig([
83
                'js' => $this->xConfigManager->getOption('js.app'),
84
                'include' => $this->xConfigManager->getOption('assets.include'),
85
            ]);
86
    }
87
88
    /**
89
     * @param string $sExt
90
     *
91
     * @return Filesystem
92
     */
93
    protected function _storage(string $sExt): Filesystem
94
    {
95
        if($this->config()->hasOption('storage'))
96
        {
97
            return $this->xStorageManager->get($this->config()->getOption('storage'));
0 ignored issues
show
Bug introduced by
It seems like $this->config()->getOption('storage') can also be of type null; however, parameter $sOptionName of Jaxon\Storage\StorageManager::get() 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

97
            return $this->xStorageManager->get(/** @scrutinizer ignore-type */ $this->config()->getOption('storage'));
Loading history...
98
        }
99
100
        $sRootDir = $this->getAssetDir($sExt);
101
        // Fylsystem options: we don't want the root dir to be created if it doesn't exist.
102
        $aAdapterOptions = ['lazyRootCreation' => true];
103
        $aDirOptions = [
104
            'config' => [
105
                'public_url' => $this->getAssetUri($sExt),
106
            ],
107
        ];
108
        return $this->xStorageManager
109
            ->adapter('local', $aAdapterOptions)
110
            ->make($sRootDir, $aDirOptions);
111
    }
112
113
    /**
114
     * @param string $sExt
115
     *
116
     * @return Filesystem
117
     */
118
    protected function storage(string $sExt): Filesystem
119
    {
120
        return $this->aStorage[$sExt] ??= $this->_storage($sExt);
121
    }
122
123
    /**
124
     * @param array $aValues
125
     *
126
     * @return string
127
     */
128
    public function makeFileOptions(array $aValues): string
129
    {
130
        if(!isset($aValues['options']) || !$aValues['options'])
131
        {
132
            return '';
133
        }
134
        if(is_array($aValues['options']))
135
        {
136
            $aOptions = [];
137
            foreach($aValues['options'] as $sName => $sValue)
138
            {
139
                $aOptions[] = "{$sName}=\"" . trim($sValue) . '"';
140
            }
141
            return implode(' ', $aOptions);
142
        }
143
        if(is_string($aValues['options']))
144
        {
145
            return trim($aValues['options']);
146
        }
147
        return '';
148
    }
149
150
    /**
151
     * Get app js options
152
     *
153
     * @return string
154
     */
155
    public function getJsOptions(): string
156
    {
157
        // Revert to the options in the "lib" section in the config,
158
        // if there is no options defined in the 'app' section.
159
        if(!$this->xConfigManager->hasAppOption('assets'))
160
        {
161
            $sOptions = trim($this->config()->getOption('js.options', ''));
162
            return $sOptions === '' ? 'charset="UTF-8"' : "$sOptions charset=\"UTF-8\"";
163
        }
164
165
        return $this->makeFileOptions([
166
            'options' => $this->config()->getOption('js.options', ''),
167
        ]);
168
    }
169
170
    /**
171
     * Get app js options
172
     *
173
     * @return string
174
     */
175
    public function getCssOptions(): string
176
    {
177
        return $this->makeFileOptions([
178
            'options' => $this->config()->getOption('css.options', ''),
179
        ]);
180
    }
181
182
    /**
183
     * Check if the assets of this plugin shall be included in Jaxon generated code.
184
     *
185
     * @param Generator|CssGenerator|JsGenerator $xGenerator
186
     *
187
     * @return bool
188
     */
189
    public function shallIncludeAssets(Generator|CssGenerator|JsGenerator $xGenerator): bool
190
    {
191
        if(!is_subclass_of($xGenerator, AbstractPlugin::class))
192
        {
193
            return true;
194
        }
195
196
        /** @var AbstractPlugin */
197
        $xPlugin = $xGenerator;
198
        $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

198
        $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 Jaxon\Plugin\JsCodeGeneratorInterface such as Jaxon\Plugin\Request\Cal...\CallableFunctionPlugin or Jaxon\Plugin\Request\Cal...ass\CallableClassPlugin. ( Ignorable by Annotation )

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

198
        $sPluginOptionName = 'include.' . $xPlugin->/** @scrutinizer ignore-call */ getName();
Loading history...
Bug introduced by
The method getName() does not exist on 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

198
        $sPluginOptionName = 'include.' . $xPlugin->/** @scrutinizer ignore-call */ getName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
199
200
        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 null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
201
            $this->config()->getOption($sPluginOptionName) :
202
            $this->config()->getOption('include.all', true);
203
    }
204
205
    /**
206
     * Get the HTML tags to include Jaxon javascript files into the page
207
     *
208
     * @return array
209
     */
210
    public function getJsLibFiles(): array
211
    {
212
        $sJsExtension = $this->config()->getOption('minify') ? '.min.js' : '.js';
213
        // The URI for the javascript library files
214
        $sJsLibUri = $this->xConfigManager->getOption('js.lib.uri', self::JS_LIB_URL);
215
        $sJsLibUri = rtrim($sJsLibUri, '/');
216
217
        // Add component files to the javascript file array.
218
        $sChibiUrl = "$sJsLibUri/libs/chibi/chibi$sJsExtension";
219
        $aJsFiles = [
220
            $this->xConfigManager->getOption('js.lib.jq', $sChibiUrl),
221
            "$sJsLibUri/jaxon.core$sJsExtension",
222
        ];
223
        if($this->xConfigManager->getOption('core.debug.on'))
224
        {
225
            $sLanguage = $this->xConfigManager->getOption('core.language');
226
            $aJsFiles[] = "$sJsLibUri/jaxon.debug$sJsExtension";
227
            $aJsFiles[] = "$sJsLibUri/lang/jaxon.$sLanguage$sJsExtension";
228
        }
229
230
        return $aJsFiles;
231
    }
232
233
    /**
234
     * @param string $sExt
235
     *
236
     * @return string
237
     */
238
    private function getAssetUri(string $sExt): string
239
    {
240
        return rtrim($this->config()->hasOption("$sExt.uri") ?
0 ignored issues
show
Bug introduced by
It seems like $this->config()->hasOpti...)->getOption('uri', '') can also be of type 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

240
        return rtrim(/** @scrutinizer ignore-type */ $this->config()->hasOption("$sExt.uri") ?
Loading history...
241
            $this->config()->getOption("$sExt.uri") :
242
            $this->config()->getOption('uri', ''), '/');
243
    }
244
245
    /**
246
     * @param string $sExt
247
     *
248
     * @return string
249
     */
250
    private function getAssetDir(string $sExt): string
251
    {
252
        return rtrim($this->config()->hasOption("$sExt.dir") ?
0 ignored issues
show
Bug introduced by
It seems like $this->config()->hasOpti...)->getOption('dir', '') can also be of type 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

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