Passed
Branch language (ead312)
by Arnaud
05:02
created

Asset::__construct()   F

Complexity

Conditions 21
Paths 198

Size

Total Lines 108
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 55
CRAP Score 22.2257

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 21
eloc 74
c 3
b 2
f 0
nc 198
nop 3
dl 0
loc 108
ccs 55
cts 64
cp 0.8594
crap 22.2257
rs 3.35

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\Collection\Page\Page;
15
use Cecil\Config;
16
use Cecil\Exception\Exception;
17
use Cecil\Util;
18
use Intervention\Image\ImageManagerStatic as ImageManager;
19
use MatthiasMullie\Minify;
20
use ScssPhp\ScssPhp\Compiler;
21
use wapmorgan\Mp3Info\Mp3Info;
22
23
class Asset implements \ArrayAccess
24
{
25
    /** @var Builder */
26
    protected $builder;
27
28
    /** @var Config */
29
    protected $config;
30
31
    /** @var array */
32
    protected $data = [];
33
34
    /** @var bool */
35
    protected $optimized = false;
36
37
    /** @var bool */
38
    protected $fingerprinted = false;
39
40
    /** @var bool */
41
    protected $compiled = false;
42
43
    /** @var bool */
44
    protected $minified = false;
45
46
    /** @var bool */
47
    protected $ignore_missing = false;
48
49
    /**
50
     * Creates an Asset from file(s) path.
51
     *
52
     * $options[
53
     *     'fingerprint'    => true,
54
     *     'minify'         => true,
55
     *     'filename'       => '',
56
     *     'ignore_missing' => false,
57
     * ];
58
     *
59
     * @param Builder      $builder
60
     * @param string|array $paths
61
     * @param array|null   $options
62
     */
63 1
    public function __construct(Builder $builder, $paths, array $options = null)
64
    {
65 1
        $this->builder = $builder;
66 1
        $this->config = $builder->getConfig();
67 1
        $paths = is_array($paths) ? $paths : [$paths];
68
        array_walk($paths, function ($path) {
69 1
            if (empty($path)) {
70
                throw new Exception('The path parameter of "asset() can\'t be empty."');
71
            }
72 1
        });
73 1
        $this->data = [
74
            'file'           => '',
75
            'filename'       => '',
76
            'path_source'    => '',
77
            'path'           => '',
78
            'ext'            => '',
79
            'type'           => '',
80
            'subtype'        => '',
81
            'size'           => 0,
82
            'content_source' => '',
83
            'content'        => '',
84
        ];
85
86
        // handles options
87 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
88 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
89 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
90 1
        $filename = '';
91 1
        $ignore_missing = false;
92 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
93 1
        $this->ignore_missing = $ignore_missing;
94
95
        // fill data array with file(s) informations
96 1
        $cache = new Cache($this->builder, 'assets');
97 1
        $cacheKey = sprintf('%s.ser', implode('_', $paths));
98 1
        if (!$cache->has($cacheKey)) {
99 1
            $pathsCount = count($paths);
100 1
            $file = [];
101 1
            for ($i = 0; $i < $pathsCount; $i++) {
102
                // loads file(s)
103 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing);
104
                // bundle: same type/ext only
105 1
                if ($i > 0) {
106 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
107
                        throw new Exception(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
108
                    }
109 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
110
                        throw new Exception(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
111
                    }
112
                }
113
                // missing allowed = empty path
114 1
                if ($file[$i]['missing']) {
115 1
                    $this->data['path'] = '';
116
117 1
                    continue;
118
                }
119
                // set data
120 1
                if ($i == 0) {
121 1
                    $this->data['file'] = $file[$i]['filepath']; // should be an array of files in case of bundle?
122 1
                    $this->data['filename'] = $file[$i]['path'];
123 1
                    $this->data['path_source'] = $file[$i]['path'];
124 1
                    $this->data['path'] = $file[$i]['path'];
125 1
                    if (!empty($filename)) {
126
                        $this->data['path'] = '/'.ltrim($filename, '/');
127
                    }
128 1
                    $this->data['ext'] = $file[$i]['ext'];
129 1
                    $this->data['type'] = $file[$i]['type'];
130 1
                    $this->data['subtype'] = $file[$i]['subtype'];
131
                }
132 1
                $this->data['size'] += $file[$i]['size'];
133 1
                $this->data['content_source'] .= $file[$i]['content'];
134 1
                $this->data['content'] .= $file[$i]['content'];
135
            }
136
            // bundle: define path
137 1
            if ($pathsCount > 1) {
138 1
                if (empty($filename)) {
139 1
                    switch ($this->data['ext']) {
140 1
                        case 'scss':
141 1
                        case 'css':
142 1
                            $this->data['path'] = '/styles.'.$file[0]['ext'];
143 1
                            break;
144
                        case 'js':
145
                            $this->data['path'] = '/scripts.'.$file[0]['ext'];
146
                            break;
147
                        default:
148
                            throw new Exception(\sprintf('Asset bundle supports "%s" files only.', 'scss, css and js'));
149
                    }
150
                }
151
            }
152 1
            $cache->set($cacheKey, $this->data);
153
        }
154 1
        $this->data = $cache->get($cacheKey);
155
156
        // optimizing
157 1
        if ($optimize) {
158
            $this->optimize();
159
        }
160
        // fingerprinting
161 1
        if ($fingerprint) {
162 1
            $this->fingerprint();
163
        }
164
        // compiling
165 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
166 1
            $this->compile();
167
        }
168
        // minifying
169 1
        if ($minify) {
170 1
            $this->minify();
171
        }
172 1
    }
173
174
    /**
175
     * Returns path.
176
     */
177 1
    public function __toString(): string
178
    {
179
        try {
180 1
            $this->save();
181
        } catch (Exception $e) {
182
            $this->builder->getLogger()->error($e->getMessage());
183
        }
184
185 1
        return $this->data['path'];
186
    }
187
188
    /**
189
     * Fingerprints a file.
190
     */
191 1
    public function fingerprint(): self
192
    {
193 1
        if ($this->fingerprinted) {
194
            return $this;
195
        }
196
197 1
        $fingerprint = hash('md5', $this->data['content_source']);
198 1
        $this->data['path'] = preg_replace(
199 1
            '/\.'.$this->data['ext'].'$/m',
200 1
            ".$fingerprint.".$this->data['ext'],
201 1
            $this->data['path']
202
        );
203
204 1
        $this->fingerprinted = true;
205
206 1
        return $this;
207
    }
208
209
    /**
210
     * Compiles a SCSS.
211
     */
212 1
    public function compile(): self
213
    {
214 1
        if ($this->compiled) {
215 1
            return $this;
216
        }
217
218 1
        if ($this->data['ext'] != 'scss') {
219 1
            return $this;
220
        }
221
222 1
        $cache = new Cache($this->builder, 'assets');
223 1
        $cacheKey = $cache->createKeyFromAsset($this, 'compiled');
224 1
        if (!$cache->has($cacheKey)) {
225 1
            $scssPhp = new Compiler();
226
            // import path
227 1
            $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath()));
228 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
229 1
            $themes = $this->config->getTheme() ?? [];
230 1
            foreach ($scssDir as $dir) {
231 1
                $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath(), $dir));
232 1
                $scssPhp->addImportPath(Util::joinPath(dirname($this->data['file']), $dir));
233 1
                foreach ($themes as $theme) {
234 1
                    $scssPhp->addImportPath(Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir")));
235
                }
236
            }
237
            // source map
238 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
239 1
                $importDir = [];
240 1
                $staticDir = (string) $this->config->get('static.dir');
241 1
                $staticDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR.$staticDir.DIRECTORY_SEPARATOR);
242 1
                $fileRelPath = substr($this->data['file'], $staticDirPos + 8);
243 1
                $filePath = Util::joinFile($this->config->getOutputPath(), $this->config->get('static.target') ?? '', $fileRelPath);
244 1
                $importDir[] = dirname($filePath);
245 1
                foreach ($scssDir as $dir) {
246 1
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $this->config->get('static.target') ?? '', $dir);
247
                }
248 1
                $scssPhp->setImportPaths(array_unique($importDir));
249 1
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
250 1
                $scssPhp->setSourceMapOptions([
251 1
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
252 1
                    'sourceRoot'        => '/',
253
                ]);
254
            }
255
            // output style
256 1
            $outputStyles = ['expanded', 'compressed'];
257 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
258 1
            if (!in_array($outputStyle, $outputStyles)) {
259
                throw new Exception(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
260
            }
261 1
            $scssPhp->setOutputStyle($outputStyle);
262
            // variables
263 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
264 1
            if (!empty($variables)) {
265 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
266 1
                $scssPhp->replaceVariables($variables);
267
            }
268
            // update data
269 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
270 1
            $this->data['ext'] = 'css';
271 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
272 1
            $this->compiled = true;
273 1
            $cache->set($cacheKey, $this->data);
274
        }
275 1
        $this->data = $cache->get($cacheKey);
276
277 1
        return $this;
278
    }
279
280
    /**
281
     * Minifying a CSS or a JS.
282
     */
283 1
    public function minify(): self
284
    {
285
        // disable minify for sourcemap
286 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
287 1
            return $this;
288
        }
289
290
        if ($this->minified) {
291
            return $this;
292
        }
293
294
        if ($this->data['ext'] == 'scss') {
295
            $this->compile();
296
        }
297
298
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
299
            return $this;
300
        }
301
302
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
303
            $this->minified;
304
305
            return $this;
306
        }
307
308
        $cache = new Cache($this->builder, 'assets');
309
        $cacheKey = $cache->createKeyFromAsset($this, 'minified');
310
        if (!$cache->has($cacheKey)) {
311
            switch ($this->data['ext']) {
312
                case 'css':
313
                    $minifier = new Minify\CSS($this->data['content']);
314
                    break;
315
                case 'js':
316
                    $minifier = new Minify\JS($this->data['content']);
317
                    break;
318
                default:
319
                    throw new Exception(sprintf('Not able to minify "%s"', $this->data['path']));
320
            }
321
            $this->data['path'] = preg_replace(
322
                '/\.'.$this->data['ext'].'$/m',
323
                '.min.'.$this->data['ext'],
324
                $this->data['path']
325
            );
326
            $this->data['content'] = $minifier->minify();
327
            $this->minified = true;
328
            $cache->set($cacheKey, $this->data);
329
        }
330
        $this->data = $cache->get($cacheKey);
331
332
        return $this;
333
    }
334
335
    /**
336
     * Optimizing an image.
337
     */
338
    public function optimize(): self
339
    {
340
        if ($this->optimized) {
341
            return $this;
342
        }
343
344
        if ($this->data['type'] != 'image') {
345
            return $this;
346
        }
347
348
        $cache = new Cache($this->builder, 'assets');
349
        $cacheKey = $cache->createKeyFromAsset($this, 'optimized');
350
        if (!$cache->has($cacheKey)) {
351
            $message = $this->data['file'];
352
            $sizeBefore = filesize($this->data['file']);
353
            Util\File::getFS()->copy($this->data['file'], Util::joinFile($this->config->getCachePath(), 'tmp', $this->data['filename']));
354
            Image::optimizer($this->config->get('assets.images.quality') ?? 85)->optimize(
355
                $this->data['file'],
356
                Util::joinFile($this->config->getCachePath(), 'tmp', $this->data['filename'])
357
            );
358
            $sizeAfter = filesize(Util::joinFile($this->config->getCachePath(), 'tmp', $this->data['filename']));
359
            if ($sizeAfter < $sizeBefore) {
360
                $message = \sprintf(
361
                    '%s (%s Ko -> %s Ko)',
362
                    $message,
363
                    ceil($sizeBefore / 1000),
364
                    ceil($sizeAfter / 1000)
365
                );
366
            }
367
            $this->data['content'] = Util\File::fileGetContents(Util::joinFile($this->config->getCachePath(), 'tmp', $this->data['filename']));
368
            Util\File::getFS()->remove(Util::joinFile($this->config->getCachePath(), 'tmp'));
369
            $this->optimized = true;
370
            $cache->set($cacheKey, $this->data);
371
            $this->builder->getLogger()->debug(\sprintf('Optimize "%s"', $message));
372
        }
373
        $this->data = $cache->get($cacheKey);
374
375
        return $this;
376
    }
377
378
    /**
379
     * Resizes an image.
380
     */
381 1
    public function resize(int $size): self
382
    {
383 1
        $cache = new Cache($this->builder, 'assets');
384 1
        $cacheKey = $cache->createKeyFromAsset($this, $size);
385 1
        if (!$cache->has($cacheKey)) {
386 1
            if ($this->data['type'] !== 'image') {
387
                throw new Exception(sprintf('Not able to resize "%s"', $this->data['path']));
388
            }
389 1
            if (!extension_loaded('gd')) {
390
                throw new Exception('GD extension is required to use images resize.');
391
            }
392
393
            try {
394 1
                $img = ImageManager::make($this->data['content_source']);
395
                $img->resize($size, null, function (\Intervention\Image\Constraint $constraint) {
396 1
                    $constraint->aspectRatio();
397 1
                    $constraint->upsize();
398 1
                });
399
            } catch (\Exception $e) {
400
                throw new Exception(sprintf('Not able to resize image "%s": %s', $this->data['path'], $e->getMessage()));
401
            }
402 1
            $this->data['path'] = '/'.Util::joinPath((string) $this->config->get('assets.target'), 'thumbnails', (string) $size, $this->data['path_source']);
403
404
            try {
405 1
                $this->data['content'] = (string) $img->encode($this->data['ext'], $this->config->get('assets.images.quality'));
406
            } catch (\Exception $e) {
407
                throw new Exception(sprintf('Not able to encode image "%s": %s', $this->data['path'], $e->getMessage()));
408
            }
409
410 1
            $cache->set($cacheKey, $this->data);
411
        }
412 1
        $this->data = $cache->get($cacheKey);
413
414 1
        return $this;
415
    }
416
417
    /**
418
     * Returns the data URL of an image.
419
     */
420
    public function dataurl(): string
421
    {
422
        if ($this->data['type'] !== 'image') {
423
            throw new Exception(sprintf('Can\'t get data URL of "%s"', $this->data['path']));
424
        }
425
426
        return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
427
    }
428
429
    /**
430
     * Implements \ArrayAccess.
431
     */
432
    public function offsetSet($offset, $value)
433
    {
434
        if (!is_null($offset)) {
435
            $this->data[$offset] = $value;
436
        }
437
    }
438
439
    /**
440
     * Implements \ArrayAccess.
441
     */
442 1
    public function offsetExists($offset)
443
    {
444 1
        return isset($this->data[$offset]);
445
    }
446
447
    /**
448
     * Implements \ArrayAccess.
449
     */
450
    public function offsetUnset($offset)
451
    {
452
        unset($this->data[$offset]);
453
    }
454
455
    /**
456
     * Implements \ArrayAccess.
457
     */
458 1
    public function offsetGet($offset)
459
    {
460 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
461
    }
462
463
    /**
464
     * Hashing content of an asset with the specified algo, sha384 by default.
465
     * Used for SRI (Subresource Integrity).
466
     *
467
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
468
     */
469 1
    public function getIntegrity(string $algo = 'sha384'): string
470
    {
471 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
472
    }
473
474
    /**
475
     * Returns the width of an image.
476
     *
477
     * @return false|int
478
     */
479 1
    public function getWidth()
480
    {
481 1
        if (false === $size = $this->getImageSize()) {
482
            return false;
483
        }
484
485 1
        return $size[0];
486
    }
487
488
    /**
489
     * Returns the height of an image.
490
     *
491
     * @return false|int
492
     */
493
    public function getHeight()
494
    {
495
        if (false === $size = $this->getImageSize()) {
496
            return false;
497
        }
498
499
        return $size[1];
500
    }
501
502
    /**
503
     * Returns MP3 file infos.
504
     *
505
     * @see https://github.com/wapmorgan/Mp3Info
506
     */
507
    public function getAudio(): Mp3Info
508
    {
509
        return new Mp3Info($this->data['file']);
510
    }
511
512
    /**
513
     * Saves file.
514
     * Note: a file from `static/` with the same name will NOT be overridden.
515
     *
516
     * @throws Exception
517
     */
518 1
    public function save(): void
519
    {
520 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
521 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
522
            try {
523 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
524 1
                $this->builder->getLogger()->debug(\sprintf('Save asset "%s"', $this->data['path']));
525
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
526
                if (!$this->ignore_missing) {
527
                    throw new Exception(\sprintf('Can\'t save asset "%s"', $this->data['path']));
528
                }
529
            }
530
        }
531 1
    }
532
533
    /**
534
     * Load file data.
535
     */
536 1
    private function loadFile(string $path, bool $ignore_missing = false): array
537
    {
538 1
        $file = [];
539
540 1
        if (false === $filePath = $this->findFile($path)) {
541 1
            if ($ignore_missing) {
542 1
                $file['missing'] = true;
543
544 1
                return $file;
545
            }
546
547 1
            throw new Exception(sprintf('Asset file "%s" doesn\'t exist.', $path));
548
        }
549
550 1
        if (Util\Url::isUrl($path)) {
551 1
            $urlHost = parse_url($path, PHP_URL_HOST);
552 1
            $urlPath = parse_url($path, PHP_URL_PATH);
553 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
554 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $urlHost, $urlPath);
555 1
            if (!empty($urlQuery)) {
556 1
                $path = Util::joinPath($path, Page::slugify($urlQuery));
557
                // Google Fonts hack
558 1
                if (strpos($urlPath, '/css') !== false) {
559 1
                    $path .= '.css';
560
                }
561
            }
562
        }
563 1
        $path = '/'.ltrim($path, '/');
564
565 1
        $pathinfo = pathinfo($path);
566 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
567 1
        $content = Util\File::fileGetContents($filePath);
568
569 1
        $file['filepath'] = $filePath;
570 1
        $file['path'] = $path;
571 1
        $file['ext'] = $pathinfo['extension'];
572 1
        $file['type'] = $type;
573 1
        $file['subtype'] = $subtype;
574 1
        $file['size'] = filesize($filePath);
575 1
        $file['content'] = $content;
576 1
        $file['missing'] = false;
577
578 1
        return $file;
579
    }
580
581
    /**
582
     * Try to find the file:
583
     *   1. remote (if $path is a valid URL)
584
     *   2. in static/
585
     *   3. in themes/<theme>/static/
586
     * Returns local file path or false if file don't exists.
587
     *
588
     * @return string|false
589
     */
590 1
    private function findFile(string $path)
591
    {
592
        // in case of remote file: save it and returns cached file path
593 1
        if (Util\Url::isUrl($path)) {
594 1
            $url = $path;
595 1
            $relativePath = Page::slugify(sprintf('%s%s-%s', parse_url($url, PHP_URL_HOST), parse_url($url, PHP_URL_PATH), parse_url($url, PHP_URL_QUERY)));
596 1
            $filePath = Util::joinFile($this->config->getCacheAssetsPath(), $relativePath);
597 1
            if (!file_exists($filePath)) {
598 1
                if (!Util\Url::isRemoteFileExists($url)) {
599 1
                    return false;
600
                }
601 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
602
                    return false;
603
                }
604 1
                if (strlen($content) <= 1) {
605
                    throw new Exception(sprintf('Asset at "%s" is empty.', $url));
606
                }
607 1
                Util\File::getFS()->dumpFile($filePath, $content);
608
            }
609
610 1
            return $filePath;
611
        }
612
613
        // checks in static/
614 1
        $filePath = Util::joinFile($this->config->getStaticPath(), $path);
615 1
        if (Util\File::getFS()->exists($filePath)) {
616 1
            return $filePath;
617
        }
618
619
        // checks in each themes/<theme>/static/
620 1
        foreach ($this->config->getTheme() as $theme) {
621 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
622 1
            if (Util\File::getFS()->exists($filePath)) {
623 1
                return $filePath;
624
            }
625
        }
626
627 1
        return false;
628
    }
629
630
    /**
631
     * Returns image size informations.
632
     *
633
     * @see https://www.php.net/manual/function.getimagesize.php
634
     *
635
     * @return false|array
636
     */
637 1
    private function getImageSize()
638
    {
639 1
        if (!$this->data['type'] == 'image') {
640
            return false;
641
        }
642
643 1
        if (false === $size = getimagesizefromstring($this->data['content'])) {
644
            return false;
645
        }
646
647 1
        return $size;
648
    }
649
}
650