Passed
Push — asset-cache ( 14efc5...2b2f29 )
by Arnaud
19:58 queued 15:25
created

Asset::minify()   B

Complexity

Conditions 10
Paths 13

Size

Total Lines 45
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10.7998

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 30
nc 13
nop 0
dl 0
loc 45
ccs 24
cts 30
cp 0.8
crap 10.7998
rs 7.6666
c 1
b 0
f 0

How to fix   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 1
    public function __construct(Builder $builder, $paths, array $options = null)
53
    {
54 1
        $this->builder = $builder;
55 1
        $this->config = $builder->getConfig();
56 1
        $paths = is_array($paths) ? $paths : [$paths];
57
58
        // handles options
59 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
60 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
61 1
        $filename = '';
62 1
        $ignore_missing = false;
63 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
64 1
        $this->ignore_missing = $ignore_missing;
65
66
        // fill data
67 1
        $cache = new Cache($this->builder, 'assets');
68 1
        $cacheKey = implode('_', $paths);
69 1
        if (!$cache->has($cacheKey)) {
70 1
            $file = [];
71 1
            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 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing);
74
                // bundle: same type/ext only
75 1
                if ($i > 0) {
76 1
                    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 1
                    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 1
                if ($file[$i]['missing']) {
85 1
                    $this->data['path'] = '';
86
87 1
                    continue;
88
                }
89
                // set data
90 1
                if ($i == 0) {
91 1
                    $this->data['file'] = $file[$i]['filepath']; // @todo: should be an array in case of bundle?
92 1
                    $this->data['path'] = $file[$i]['path'];
93 1
                    if (!empty($filename)) {
94
                        $this->data['path'] = '/'.ltrim($filename, '/');
95
                    }
96 1
                    $this->data['ext'] = $file[$i]['ext'];
97 1
                    $this->data['type'] = $file[$i]['type'];
98 1
                    $this->data['subtype'] = $file[$i]['subtype'];
99
                }
100 1
                $this->data['size'] += $file[$i]['size'];
101 1
                $this->data['source'] .= $file[$i]['content'];
102 1
                $this->data['content'] .= $file[$i]['content'];
103
            }
104
            // bundle: define path
105 1
            if (count($paths) > 1) {
106 1
                if (empty($filename)) {
1 ignored issue
show
introduced by
The condition empty($filename) is always true.
Loading history...
107 1
                    switch ($this->data['ext']) {
108 1
                        case 'scss':
109 1
                        case 'css':
110 1
                            $this->data['path'] = '/styles.'.$file[0]['ext'];
111 1
                            break;
112
                        case 'js':
113
                            $this->data['path'] = '/scripts.'.$file[0]['ext'];
114
                            break;
115
                        default:
116
                            throw new Exception(\sprintf('Asset bundle supports "%s" files only.', 'scss, css and js'));
117
                    }
118
                }
119
            }
120 1
            $cache->set($cacheKey, $this->data);
121
        }
122 1
        $this->data = $cache->get($cacheKey);
123
124
        // fingerprinting
125 1
        if ($fingerprint) {
126 1
            $this->fingerprint();
127
        }
128
        // compiling
129 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
130 1
            $this->compile();
131
        }
132
        // minifying
133 1
        if ($minify) {
134 1
            $this->minify();
135
        }
136 1
    }
137
138
    /**
139
     * Returns path.
140
     *
141
     * @return string
142
     */
143 1
    public function __toString(): string
144
    {
145 1
        $this->save();
146
147 1
        return $this->data['path'];
148
    }
149
150
    /**
151
     * Fingerprints a file.
152
     *
153
     * @return self
154
     */
155 1
    public function fingerprint(): self
156
    {
157 1
        if ($this->fingerprinted) {
158
            return $this;
159
        }
160
161 1
        $fingerprint = hash('md5', $this->data['source']);
162 1
        $this->data['path'] = preg_replace(
163 1
            '/\.'.$this->data['ext'].'$/m',
164 1
            ".$fingerprint.".$this->data['ext'],
165 1
            $this->data['path']
166
        );
167
168 1
        $this->fingerprinted = true;
169
170 1
        return $this;
171
    }
172
173
    /**
174
     * Compiles a SCSS.
175
     *
176
     * @return self
177
     */
178 1
    public function compile(): self
179
    {
180 1
        if ($this->compiled) {
181 1
            return $this;
182
        }
183
184 1
        if ($this->data['ext'] != 'scss') {
185 1
            return $this;
186
        }
187
188 1
        $cache = new Cache($this->builder, 'assets');
189 1
        $cacheKey = $cache->createKeyFromAsset($this);
190 1
        if (!$cache->has($cacheKey)) {
191 1
            $scssPhp = new Compiler();
192
            // import path
193 1
            $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath()));
194 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
195 1
            $themes = $this->config->getTheme() ?? [];
196 1
            foreach ($scssDir as $dir) {
197 1
                $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath(), $dir));
198 1
                $scssPhp->addImportPath(Util::joinPath(dirname($this->data['file']), $dir));
199 1
                foreach ($themes as $theme) {
200 1
                    $scssPhp->addImportPath(Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir")));
201
                }
202
            }
203
            // output style
204 1
            $outputStyles = ['expanded', 'compressed'];
205 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
206 1
            if (!in_array($outputStyle, $outputStyles)) {
207
                throw new Exception(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
208
            }
209 1
            $scssPhp->setOutputStyle($outputStyle);
210
            // variables
211 1
            $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

211
            /** @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...
212
            // update data
213 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
214 1
            $this->data['ext'] = 'css';
215 1
            $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

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