Passed
Push — main ( ee6fb2...96b9cc )
by Thierry
05:15
created

AssetManager::getAssetUri()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
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\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'));
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

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