Passed
Push — analysis-gOJ73E ( 71f184 )
by Arnaud
08:35 queued 02:45
created

Asset::__construct()   D

Complexity

Conditions 19
Paths 102

Size

Total Lines 84
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 2 Features 0
Metric Value
cc 19
eloc 54
nc 102
nop 3
dl 0
loc 84
rs 4.5
c 10
b 2
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the Cecil/Cecil package.
4
 *
5
 * Copyright (c) Arnaud Ligny <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Cecil\Assets;
12
13
use Cecil\Builder;
14
use Cecil\Config;
15
use Cecil\Exception\Exception;
16
use Cecil\Util;
17
use MatthiasMullie\Minify;
18
use ScssPhp\ScssPhp\Compiler;
19
use wapmorgan\Mp3Info\Mp3Info;
20
21
class Asset implements \ArrayAccess
22
{
23
    /** @var Builder */
24
    protected $builder;
25
    /** @var Config */
26
    protected $config;
27
    /** @var array */
28
    protected $data = [];
29
    /** @var bool */
30
    protected $fingerprinted = false;
31
    /** @var bool */
32
    protected $compiled = false;
33
    /** @var bool */
34
    protected $minified = false;
35
    /** @var bool */
36
    protected $ignore_missing = false;
37
38
    /**
39
     * Creates an Asset from file(s) path.
40
     *
41
     * $options[
42
     *     'fingerprint'    => true,
43
     *     'minify'         => true,
44
     *     'filename'       => '',
45
     *     'ignore_missing' => false,
46
     * ];
47
     *
48
     * @param Builder      $builder
49
     * @param string|array $paths
50
     * @param array|null   $options
51
     */
52
    public function __construct(Builder $builder, $paths, array $options = null)
53
    {
54
        $this->builder = $builder;
55
        $this->config = $builder->getConfig();
56
        $paths = is_array($paths) ? $paths : [$paths];
57
58
        // handles options
59
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
60
        $minify = (bool) $this->config->get('assets.minify.enabled');
61
        $filename = '';
62
        $ignore_missing = false;
63
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
64
        $this->ignore_missing = $ignore_missing;
65
66
        // fill data
67
        $cache = new Cache($this->builder, 'assets');
68
        $cacheKey = implode('_', $paths);
69
        if (!$cache->has($cacheKey)) {
70
            $file = [];
71
            for ($i = 0; $i < count($paths); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
72
                // loads file(s)
73
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing);
74
                // bundle: same type/ext only
75
                if ($i > 0) {
76
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
77
                        throw new Exception(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
78
                    }
79
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
80
                        throw new Exception(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
81
                    }
82
                }
83
                // missing allowed = empty path
84
                if ($file[$i]['missing']) {
85
                    $this->data['path'] = '';
86
87
                    continue;
88
                }
89
                // set data
90
                if ($i == 0) {
91
                    $this->data['file'] = $file[$i]['filepath']; // @todo: should be an array in case of bundle?
92
                    $this->data['path'] = $file[$i]['path'];
93
                    if (!empty($filename)) {
94
                        $this->data['path'] = '/'.ltrim($filename, '/');
95
                    }
96
                    $this->data['ext'] = $file[$i]['ext'];
97
                    $this->data['type'] = $file[$i]['type'];
98
                    $this->data['subtype'] = $file[$i]['subtype'];
99
                }
100
                $this->data['size'] += $file[$i]['size'];
101
                $this->data['source'] .= $file[$i]['content'];
102
                $this->data['content'] .= $file[$i]['content'];
103
            }
104
            // bundle: define path
105
            if (count($paths) > 1) {
106
                //$this->data['path'] = '/'.ltrim($filename, '/');
107
                if (empty($filename)) {
108
                    switch ($this->data['ext']) {
109
                        case 'scss':
110
                        case 'css':
111
                            $this->data['path'] = '/styles.'.$file[0]['ext'];
112
                            break;
113
                        case 'js':
114
                            $this->data['path'] = '/scripts.'.$file[0]['ext'];
115
                            break;
116
                        default:
117
                            throw new Exception(\sprintf('Asset bundle supports "%s" files only.', 'scss, css and js'));
118
                    }
119
                }
120
            }
121
            $cache->set($cacheKey, $this->data);
122
        }
123
        $this->data = $cache->get($cacheKey);
124
125
        // fingerprinting
126
        if ($fingerprint) {
127
            $this->fingerprint();
128
        }
129
        // compiling
130
        if ((bool) $this->config->get('assets.compile.enabled')) {
131
            $this->compile();
132
        }
133
        // minifying
134
        if ($minify) {
135
            $this->minify();
136
        }
137
    }
138
139
    /**
140
     * Returns path.
141
     *
142
     * @return string
143
     */
144
    public function __toString(): string
145
    {
146
        $this->save();
147
148
        return $this->data['path'];
149
    }
150
151
    /**
152
     * Fingerprints a file.
153
     *
154
     * @return self
155
     */
156
    public function fingerprint(): self
157
    {
158
        if ($this->fingerprinted) {
159
            return $this;
160
        }
161
162
        $fingerprint = hash('md5', $this->data['source']);
163
        $this->data['path'] = preg_replace(
164
            '/\.'.$this->data['ext'].'$/m',
165
            ".$fingerprint.".$this->data['ext'],
166
            $this->data['path']
167
        );
168
169
        $this->fingerprinted = true;
170
171
        return $this;
172
    }
173
174
    /**
175
     * Compiles a SCSS.
176
     *
177
     * @return self
178
     */
179
    public function compile(): self
180
    {
181
        if ($this->compiled) {
182
            return $this;
183
        }
184
185
        if ($this->data['ext'] != 'scss') {
186
            return $this;
187
        }
188
189
        $cache = new Cache($this->builder, 'assets');
190
        $cacheKey = $cache->createKeyFromAsset($this);
191
        if (!$cache->has($cacheKey)) {
192
            $scssPhp = new Compiler();
193
            // import path
194
            $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath()));
195
            $scssDir = $this->config->get('assets.compile.import') ?? [];
196
            $themes = $this->config->getTheme() ?? [];
197
            foreach ($scssDir as $dir) {
198
                $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath(), $dir));
199
                $scssPhp->addImportPath(Util::joinPath(dirname($this->data['file']), $dir));
200
                foreach ($themes as $theme) {
201
                    $scssPhp->addImportPath(Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir")));
202
                }
203
            }
204
            // output style
205
            $outputStyles = ['expanded', 'compressed'];
206
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
207
            if (!in_array($outputStyle, $outputStyles)) {
208
                throw new Exception(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
209
            }
210
            $scssPhp->setOutputStyle($outputStyle);
211
            // variables
212
            $scssPhp->setVariables($this->config->get('assets.compile.variables') ?? []);
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::setVariables() has been deprecated: Use "addVariables" or "replaceVariables" instead. ( Ignorable by Annotation )

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

212
            /** @scrutinizer ignore-deprecated */ $scssPhp->setVariables($this->config->get('assets.compile.variables') ?? []);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
213
            // update data
214
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
215
            $this->data['ext'] = 'css';
216
            $this->data['content'] = $scssPhp->compile($this->data['content']);
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::compile() has been deprecated: Use {@see compileString} instead. ( Ignorable by Annotation )

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

216
            $this->data['content'] = /** @scrutinizer ignore-deprecated */ $scssPhp->compile($this->data['content']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
217
            $this->compiled = true;
218
            $cache->set($cacheKey, $this->data);
219
        }
220
        $this->data = $cache->get($cacheKey);
221
222
        return $this;
223
    }
224
225
    /**
226
     * Minifying a CSS or a JS.
227
     *
228
     * @return self
229
     */
230
    public function minify(): self
231
    {
232
        if ($this->minified) {
233
            return $this;
234
        }
235
236
        if ($this->data['ext'] == 'scss') {
237
            $this->compile();
238
        }
239
240
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
241
            return $this;
242
        }
243
244
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
245
            $this->minified;
246
247
            return $this;
248
        }
249
250
        $cache = new Cache($this->builder, 'assets');
251
        $cacheKey = $cache->createKeyFromAsset($this);
252
        if (!$cache->has($cacheKey)) {
253
            switch ($this->data['ext']) {
254
                case 'css':
255
                    $minifier = new Minify\CSS($this->data['content']);
256
                    break;
257
                case 'js':
258
                    $minifier = new Minify\JS($this->data['content']);
259
                    break;
260
                default:
261
                    throw new Exception(sprintf('Not able to minify "%s"', $this->data['path']));
262
            }
263
            $this->data['path'] = preg_replace(
264
                '/\.'.$this->data['ext'].'$/m',
265
                '.min.'.$this->data['ext'],
266
                $this->data['path']
267
            );
268
            $this->data['content'] = $minifier->minify();
269
            $this->minified = true;
270
            $cache->set($cacheKey, $this->data);
271
        }
272
        $this->data = $cache->get($cacheKey);
273
274
        return $this;
275
    }
276
277
    /**
278
     * Implements \ArrayAccess.
279
     */
280
    public function offsetSet($offset, $value)
281
    {
282
        if (!is_null($offset)) {
283
            $this->data[$offset] = $value;
284
        }
285
    }
286
287
    /**
288
     * Implements \ArrayAccess.
289
     */
290
    public function offsetExists($offset)
291
    {
292
        return isset($this->data[$offset]);
293
    }
294
295
    /**
296
     * Implements \ArrayAccess.
297
     */
298
    public function offsetUnset($offset)
299
    {
300
        unset($this->data[$offset]);
301
    }
302
303
    /**
304
     * Implements \ArrayAccess.
305
     */
306
    public function offsetGet($offset)
307
    {
308
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
309
    }
310
311
    /**
312
     * Hashing content of an asset with the specified algo, sha384 by default.
313
     * Used for SRI (Subresource Integrity).
314
     *
315
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
316
     *
317
     * @return string
318
     */
319
    public function getIntegrity(string $algo = 'sha384'): string
320
    {
321
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
322
    }
323
324
    /**
325
     * Returns the width of an image.
326
     *
327
     * @return false|int
328
     */
329
    public function getWidth()
330
    {
331
        if (false === $size = $this->getImageSize()) {
332
            return false;
333
        }
334
335
        return $size[0];
336
    }
337
338
    /**
339
     * Returns the height of an image.
340
     *
341
     * @return false|int
342
     */
343
    public function getHeight()
344
    {
345
        if (false === $size = $this->getImageSize()) {
346
            return false;
347
        }
348
349
        return $size[1];
350
    }
351
352
    /**
353
     * Returns MP3 file infos.
354
     *
355
     * @see https://github.com/wapmorgan/Mp3Info
356
     *
357
     * @return Mp3Info
358
     */
359
    public function getAudio(): Mp3Info
360
    {
361
        return new Mp3Info($this->data['file']);
362
    }
363
364
    /**
365
     * Saves file.
366
     * Note: a file from `static/` with the same name will be overridden.
367
     *
368
     * @throws Exception
369
     *
370
     * @return void
371
     */
372
    public function save(): void
373
    {
374
        $file = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
375
        if (!$this->builder->getBuildOptions()['dry-run']) {
376
            try {
377
                Util\File::getFS()->dumpFile($file, $this->data['content']);
378
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
379
                if (!$this->ignore_missing) {
380
                    throw new Exception(\sprintf('Can\'t save asset "%s"', $this->data['path']));
381
                }
382
            }
383
        }
384
    }
385
386
    /**
387
     * Load file data.
388
     *
389
     * @param string $path           Relative path or URL.
390
     * @param bool   $ignore_missing Don't throw exception if file is missing.
391
     *
392
     * @return array
393
     */
394
    private function loadFile(string $path, bool $ignore_missing = false): array
395
    {
396
        $file = [];
397
398
        if (false === $filePath = $this->findFile($path)) {
399
            if ($ignore_missing) {
400
                $file['missing'] = true;
401
402
                return $file;
403
            }
404
405
            throw new Exception(sprintf('Asset file "%s" doesn\'t exist.', $path));
406
        }
407
408
        if (Util\Url::isUrl($path)) {
409
            $path = Util::joinPath('assets', parse_url($path, PHP_URL_HOST), parse_url($path, PHP_URL_PATH));
410
        }
411
        $path = '/'.ltrim($path, '/');
412
413
        $pathinfo = pathinfo($path);
414
        list($type, $subtype) = Util\File::getMimeType($filePath);
415
        $content = Util\File::fileGetContents($filePath);
416
417
        $file['filepath'] = $filePath;
418
        $file['path'] = $path;
419
        $file['ext'] = $pathinfo['extension'];
420
        $file['type'] = $type;
421
        $file['subtype'] = $subtype;
422
        $file['size'] = filesize($filePath);
423
        $file['content'] = $content;
424
425
        return $file;
426
    }
427
428
    /**
429
     * Try to find the file:
430
     *   1. remote (if $path is a valid URL)
431
     *   2. in static/
432
     *   3. in themes/<theme>/static/
433
     * Returns local file path or false if file don't exists.
434
     *
435
     * @param string $path
436
     *
437
     * @return string|false
438
     */
439
    private function findFile(string $path)
440
    {
441
        // in case of remote file: save it and returns cached file path
442
        if (Util\Url::isUrl($path)) {
443
            $url = $path;
444
            $cache = new Cache($this->builder, 'assets');
445
            $relativePath = parse_url($url, PHP_URL_HOST).parse_url($url, PHP_URL_PATH);
446
            $filePath = Util::joinFile($this->config->getCacheAssetsPath(), $relativePath);
447
            $cacheKey = $cache->createKeyFromPath($url, $relativePath);
448
            if (!$cache->has($cacheKey) || !file_exists($filePath)) {
449
                if (!Util\Url::isRemoteFileExists($url)) {
450
                    return false;
451
                }
452
                if (false === $content = Util\File::fileGetContents($url)) {
453
                    return false;
454
                }
455
                if (strlen($content) <= 1) {
456
                    throw new Exception(sprintf('Asset at "%s" is empty.', $url));
457
                }
458
                $cache->set($cacheKey, $content);
459
                Util\File::getFS()->dumpFile($filePath, $cache->get($cacheKey));
460
            }
461
462
            return $filePath;
463
        }
464
465
        // checks in static/
466
        $filePath = Util::joinFile($this->config->getStaticPath(), $path);
467
        if (Util\File::getFS()->exists($filePath)) {
468
            return $filePath;
469
        }
470
471
        // checks in each themes/<theme>/static/
472
        foreach ($this->config->getTheme() as $theme) {
473
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
474
            if (Util\File::getFS()->exists($filePath)) {
475
                return $filePath;
476
            }
477
        }
478
479
        return false;
480
    }
481
482
    /**
483
     * Returns image size informations.
484
     *
485
     * See https://www.php.net/manual/function.getimagesize.php
486
     *
487
     * @return false|array
488
     */
489
    private function getImageSize()
490
    {
491
        if (!$this->data['type'] == 'image') {
492
            return false;
493
        }
494
495
        if (false === $size = getimagesizefromstring($this->data['content'])) {
496
            return false;
497
        }
498
499
        return $size;
500
    }
501
}
502