Passed
Push — master ( 050960...df4a53 )
by Arnaud
05:06
created

Asset::__construct()   F

Complexity

Conditions 25
Paths 166

Size

Total Lines 129
Code Lines 91

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 27.6229

Importance

Changes 7
Bugs 5 Features 0
Metric Value
cc 25
eloc 91
c 7
b 5
f 0
nc 166
nop 3
dl 0
loc 129
ccs 78
cts 93
cp 0.8387
crap 27.6229
rs 3.6166

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
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Assets\Image\Optimizer;
17
use Cecil\Builder;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Config;
20
use Cecil\Exception\RuntimeException;
21
use Cecil\Util;
22
use Intervention\Image\ImageManagerStatic as ImageManager;
23
use MatthiasMullie\Minify;
24
use ScssPhp\ScssPhp\Compiler;
25
use wapmorgan\Mp3Info\Mp3Info;
26
27
class Asset implements \ArrayAccess
28
{
29
    /** @var Builder */
30
    protected $builder;
31
32
    /** @var Config */
33
    protected $config;
34
35
    /** @var array */
36
    protected $data = [];
37
38
    /** @var bool */
39
    protected $fingerprinted = false;
40
41
    /** @var bool */
42
    protected $compiled = false;
43
44
    /** @var bool */
45
    protected $minified = false;
46
47
    /** @var bool */
48
    protected $optimize = false;
49
    protected $optimized = false;
50
51
    /** @var bool */
52
    protected $ignore_missing = false;
53
54
    /**
55
     * Creates an Asset from a file path or an array of files path.
56
     *
57
     * @param Builder      $builder
58
     * @param string|array $paths
59
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
60
     *
61
     * @throws RuntimeException
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 1
        array_walk($paths, function ($path) {
69 1
            if (!is_string($path)) {
70
                throw new RuntimeException(\sprintf('The path to an asset must be a string (%s given).', gettype($path)));
71
            }
72 1
            if (empty($path)) {
73
                throw new RuntimeException('The path to an asset can\'t be empty.');
74
            }
75 1
            if (substr($path, 0, 2) == '..') {
76
                throw new RuntimeException(\sprintf('The path to asset "%s" is wrong: it must be directly relative to "assets" or "static" directory, or a remote URL.', $path));
77
            }
78 1
        });
79 1
        $this->data = [
80 1
            'file'           => '',    // absolute file path
81 1
            'files'          => [],    // bundle: files path
82 1
            'filename'       => '',    // filename
83 1
            'path_source'    => '',    // public path to the file, before transformations
84 1
            'path'           => '',    // public path to the file, after transformations
85 1
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
86 1
            'ext'            => '',    // file extension
87 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
88 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
89 1
            'size'           => 0,     // file size (in bytes)
90 1
            'content_source' => '',    // file content, before transformations
91 1
            'content'        => '',    // file content, after transformations
92 1
            'width'          => 0,     // width (in pixels) in case of an image
93 1
            'height'         => 0,     // height (in pixels) in case of an image
94 1
            'exif'           => [],    // exif data
95 1
        ];
96
97
        // handles options
98 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
99 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
100 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
101 1
        $filename = '';
102 1
        $ignore_missing = false;
103 1
        $remote_fallback = null;
104 1
        $force_slash = true;
105 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
106 1
        $this->ignore_missing = $ignore_missing;
107
108
        // fill data array with file(s) informations
109 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
110 1
        $cacheKey = \sprintf('%s__%s', implode('_', $paths), $this->builder->getVersion());
111 1
        if (!$cache->has($cacheKey)) {
112 1
            $pathsCount = count($paths);
113 1
            $file = [];
114 1
            for ($i = 0; $i < $pathsCount; $i++) {
115
                // loads file(s)
116 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing, $remote_fallback, $force_slash);
117
                // bundle: same type/ext only
118 1
                if ($i > 0) {
119 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
120
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
121
                    }
122 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
123
                        throw new RuntimeException(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
124
                    }
125
                }
126
                // missing allowed = empty path
127 1
                if ($file[$i]['missing']) {
128 1
                    $this->data['missing'] = true;
129 1
                    $this->data['path'] = $file[$i]['path'];
130
131 1
                    continue;
132
                }
133
                // set data
134 1
                $this->data['size'] += $file[$i]['size'];
135 1
                $this->data['content_source'] .= $file[$i]['content'];
136 1
                $this->data['content'] .= $file[$i]['content'];
137 1
                if ($i == 0) {
138 1
                    $this->data['file'] = $file[$i]['filepath'];
139 1
                    $this->data['filename'] = $file[$i]['path'];
140 1
                    $this->data['path_source'] = $file[$i]['path'];
141 1
                    $this->data['path'] = $file[$i]['path'];
142 1
                    if (!empty($filename)) { /** @phpstan-ignore-line */
143 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
144
                    }
145 1
                    $this->data['ext'] = $file[$i]['ext'];
146 1
                    $this->data['type'] = $file[$i]['type'];
147 1
                    $this->data['subtype'] = $file[$i]['subtype'];
148 1
                    if ($this->data['type'] == 'image') {
149 1
                        $this->data['width'] = $this->getWidth();
150 1
                        $this->data['height'] = $this->getHeight();
151 1
                        if ($this->data['subtype'] == 'jpeg') {
152
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
153
                        }
154
                    }
155
                }
156
                // bundle files path
157 1
                $this->data['files'][] = $file[$i]['filepath'];
158
            }
159
            // bundle: define path
160 1
            if ($pathsCount > 1 && empty($filename)) { /** @phpstan-ignore-line */
161
                switch ($this->data['ext']) {
162
                    case 'scss':
163
                    case 'css':
164
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
165
                        break;
166
                    case 'js':
167
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
168
                        break;
169
                    default:
170
                        throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
171
                }
172
            }
173 1
            $cache->set($cacheKey, $this->data);
174
        }
175 1
        $this->data = $cache->get($cacheKey);
176
177
        // fingerprinting
178 1
        if ($fingerprint) {
179 1
            $this->fingerprint();
180
        }
181
        // compiling (Sass files)
182 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
183 1
            $this->compile();
184
        }
185
        // minifying (CSS and JavScript files)
186 1
        if ($minify) {
187 1
            $this->minify();
188
        }
189
        // optimizing (images files)
190 1
        if ($optimize) {
191 1
            $this->optimize = true;
192
        }
193
    }
194
195
    /**
196
     * Returns path.
197
     *
198
     * @throws RuntimeException
199
     */
200 1
    public function __toString(): string
201
    {
202
        try {
203 1
            $this->save();
204
        } catch (\Exception $e) {
205
            $this->builder->getLogger()->error($e->getMessage());
206
        }
207
208 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
209
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
210
        }
211
212 1
        return $this->data['path'];
213
    }
214
215
    /**
216
     * Fingerprints a file.
217
     */
218 1
    public function fingerprint(): self
219
    {
220 1
        if ($this->fingerprinted) {
221 1
            return $this;
222
        }
223
224 1
        $fingerprint = hash('md5', $this->data['content_source']);
225 1
        $this->data['path'] = preg_replace(
226 1
            '/\.' . $this->data['ext'] . '$/m',
227 1
            ".$fingerprint." . $this->data['ext'],
228 1
            $this->data['path']
229 1
        );
230
231 1
        $this->fingerprinted = true;
232
233 1
        return $this;
234
    }
235
236
    /**
237
     * Compiles a SCSS.
238
     *
239
     * @throws RuntimeException
240
     */
241 1
    public function compile(): self
242
    {
243 1
        if ($this->compiled) {
244 1
            return $this;
245
        }
246
247 1
        if ($this->data['ext'] != 'scss') {
248 1
            return $this;
249
        }
250
251 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
252 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
253 1
        if (!$cache->has($cacheKey)) {
254 1
            $scssPhp = new Compiler();
255 1
            $importDir = [];
256 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
257 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
258 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
259 1
            $themes = $this->config->getTheme() ?? [];
260 1
            foreach ($scssDir as $dir) {
261 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
262 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
263 1
                $importDir[] = Util::joinPath(dirname($this->data['file']), $dir);
264 1
                foreach ($themes as $theme) {
265 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
266 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
267
                }
268
            }
269 1
            $scssPhp->setImportPaths(array_unique($importDir));
270
            // source map
271 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
272
                $importDir = [];
273
                $assetDir = (string) $this->config->get('assets.dir');
274
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
275
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
276
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
277
                $importDir[] = dirname($filePath);
278
                foreach ($scssDir as $dir) {
279
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
280
                }
281
                $scssPhp->setImportPaths(array_unique($importDir));
282
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
283
                $scssPhp->setSourceMapOptions([
284
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
285
                    'sourceRoot'        => '/',
286
                ]);
287
            }
288
            // output style
289 1
            $outputStyles = ['expanded', 'compressed'];
290 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
291 1
            if (!in_array($outputStyle, $outputStyles)) {
292
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
293
            }
294 1
            $scssPhp->setOutputStyle($outputStyle);
295
            // variables
296 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
297 1
            if (!empty($variables)) {
298 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
299 1
                $scssPhp->replaceVariables($variables);
300
            }
301
            // update data
302 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
303 1
            $this->data['ext'] = 'css';
304 1
            $this->data['type'] = 'text';
305 1
            $this->data['subtype'] = 'text/css';
306 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
307 1
            $this->data['size'] = strlen($this->data['content']);
308 1
            $this->compiled = true;
309 1
            $cache->set($cacheKey, $this->data);
310
        }
311 1
        $this->data = $cache->get($cacheKey);
312
313 1
        return $this;
314
    }
315
316
    /**
317
     * Minifying a CSS or a JS.
318
     *
319
     * @throws RuntimeException
320
     */
321 1
    public function minify(): self
322
    {
323
        // disable minify to preserve inline source map
324 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
325
            return $this;
326
        }
327
328 1
        if ($this->minified) {
329 1
            return $this;
330
        }
331
332 1
        if ($this->data['ext'] == 'scss') {
333
            $this->compile();
334
        }
335
336 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
337 1
            return $this;
338
        }
339
340 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
341 1
            $this->minified;
342
343 1
            return $this;
344
        }
345
346 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
347 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
348 1
        if (!$cache->has($cacheKey)) {
349 1
            switch ($this->data['ext']) {
350 1
                case 'css':
351 1
                    $minifier = new Minify\CSS($this->data['content']);
352 1
                    break;
353 1
                case 'js':
354 1
                    $minifier = new Minify\JS($this->data['content']);
355 1
                    break;
356
                default:
357
                    throw new RuntimeException(\sprintf('Not able to minify "%s"', $this->data['path']));
358
            }
359 1
            $this->data['path'] = preg_replace(
360 1
                '/\.' . $this->data['ext'] . '$/m',
361 1
                '.min.' . $this->data['ext'],
362 1
                $this->data['path']
363 1
            );
364 1
            $this->data['content'] = $minifier->minify();
365 1
            $this->data['size'] = strlen($this->data['content']);
366 1
            $this->minified = true;
367 1
            $cache->set($cacheKey, $this->data);
368
        }
369 1
        $this->data = $cache->get($cacheKey);
370
371 1
        return $this;
372
    }
373
374
    /**
375
     * Optimizing an image.
376
     */
377 1
    public function optimize(string $filepath): self
378
    {
379 1
        if ($this->data['type'] != 'image') {
380 1
            return $this;
381
        }
382
383 1
        $quality = $this->config->get('assets.images.quality') ?? 75;
384 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
385 1
        $tags = ["q$quality", 'optimized'];
386 1
        if ($this->data['width']) {
387 1
            array_unshift($tags, "{$this->data['width']}x");
388
        }
389 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
390 1
        if (!$cache->has($cacheKey)) {
391 1
            $message = $this->data['path'];
392 1
            $sizeBefore = filesize($filepath);
393 1
            Optimizer::create($quality)->optimize($filepath);
394 1
            $sizeAfter = filesize($filepath);
395 1
            if ($sizeAfter < $sizeBefore) {
396
                $message = \sprintf(
397
                    '%s (%s Ko -> %s Ko)',
398
                    $message,
399
                    ceil($sizeBefore / 1000),
400
                    ceil($sizeAfter / 1000)
401
                );
402
            }
403 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
404 1
            $this->data['size'] = $sizeAfter;
405 1
            $cache->set($cacheKey, $this->data);
406 1
            $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
407
        }
408 1
        $this->data = $cache->get($cacheKey);
409 1
        $this->optimized = true;
410
411 1
        return $this;
412
    }
413
414
    /**
415
     * Resizes an image with a new $width.
416
     *
417
     * @throws RuntimeException
418
     */
419 1
    public function resize(int $width): self
420
    {
421 1
        if ($this->data['missing']) {
422
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
423
        }
424 1
        if ($this->data['type'] != 'image') {
425
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
426
        }
427 1
        if ($width >= $this->data['width']) {
428
            return $this;
429
        }
430
431 1
        $assetResized = clone $this;
432 1
        $assetResized->data['width'] = $width;
433
434 1
        $quality = $this->config->get('assets.images.quality');
435 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
436 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
437 1
        if (!$cache->has($cacheKey)) {
438 1
            if ($assetResized->data['type'] !== 'image') {
439
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
440
            }
441 1
            if (!extension_loaded('gd')) {
442
                throw new RuntimeException('GD extension is required to use images resize.');
443
            }
444
445
            try {
446 1
                $img = ImageManager::make($assetResized->data['content_source']);
447 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
448 1
                    $constraint->aspectRatio();
449 1
                    $constraint->upsize();
450 1
                });
451
            } catch (\Exception $e) {
452
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
453
            }
454 1
            $assetResized->data['path'] = '/' . Util::joinPath(
455 1
                (string) $this->config->get('assets.target'),
456 1
                (string) $this->config->get('assets.images.resize.dir'),
457 1
                (string) $width,
458 1
                $assetResized->data['path']
459 1
            );
460
461
            try {
462 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
463 1
                $assetResized->data['height'] = $assetResized->getHeight();
464 1
                $assetResized->data['size'] = strlen($assetResized->data['content']);
465
            } catch (\Exception $e) {
466
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
467
            }
468
469 1
            $cache->set($cacheKey, $assetResized->data);
470
        }
471 1
        $assetResized->data = $cache->get($cacheKey);
472
473 1
        return $assetResized;
474
    }
475
476
    /**
477
     * Converts an image asset to WebP format.
478
     *
479
     * @throws RuntimeException
480
     */
481 1
    public function webp(?int $quality = null): self
482
    {
483 1
        if ($this->data['type'] !== 'image') {
484
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
485
        }
486
487 1
        if ($quality === null) {
488 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
489
        }
490
491 1
        $assetWebp = clone $this;
492 1
        $format = 'webp';
493 1
        $image = ImageManager::make($assetWebp['content']);
494 1
        $assetWebp['content'] = (string) $image->encode($format, $quality);
495
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
496
        $assetWebp['ext'] = $format;
497
        $assetWebp['subtype'] = "image/$format";
498
        $assetWebp['size'] = strlen($assetWebp['content']);
0 ignored issues
show
Bug introduced by
It seems like $assetWebp['content'] can also be of type null; however, parameter $string of strlen() 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

498
        $assetWebp['size'] = strlen(/** @scrutinizer ignore-type */ $assetWebp['content']);
Loading history...
499
500
        return $assetWebp;
501
    }
502
503
    /**
504
     * Implements \ArrayAccess.
505
     */
506
    #[\ReturnTypeWillChange]
507
    public function offsetSet($offset, $value): void
508
    {
509
        if (!is_null($offset)) {
510
            $this->data[$offset] = $value;
511
        }
512
    }
513
514
    /**
515
     * Implements \ArrayAccess.
516
     */
517
    #[\ReturnTypeWillChange]
518 1
    public function offsetExists($offset): bool
519
    {
520 1
        return isset($this->data[$offset]);
521
    }
522
523
    /**
524
     * Implements \ArrayAccess.
525
     */
526
    #[\ReturnTypeWillChange]
527
    public function offsetUnset($offset): void
528
    {
529
        unset($this->data[$offset]);
530
    }
531
532
    /**
533
     * Implements \ArrayAccess.
534
     */
535
    #[\ReturnTypeWillChange]
536 1
    public function offsetGet($offset)
537
    {
538 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
539
    }
540
541
    /**
542
     * Hashing content of an asset with the specified algo, sha384 by default.
543
     * Used for SRI (Subresource Integrity).
544
     *
545
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
546
     */
547 1
    public function getIntegrity(string $algo = 'sha384'): string
548
    {
549 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
550
    }
551
552
    /**
553
     * Returns MP3 file infos.
554
     *
555
     * @see https://github.com/wapmorgan/Mp3Info
556
     */
557
    public function getAudio(): Mp3Info
558
    {
559
        if ($this->data['type'] !== 'audio') {
560
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
561
        }
562
563
        return new Mp3Info($this->data['file']);
564
    }
565
566
    /**
567
     * Returns the data URL of an image.
568
     *
569
     * @throws RuntimeException
570
     */
571 1
    public function dataurl(): string
572
    {
573 1
        if ($this->data['type'] !== 'image') {
574
            throw new RuntimeException(\sprintf('Can\'t get data URL of "%s"', $this->data['path']));
575
        }
576
577 1
        return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
578
    }
579
580
    /**
581
     * Saves file.
582
     * Note: a file from `static/` with the same name will NOT be overridden.
583
     *
584
     * @throws RuntimeException
585
     */
586 1
    public function save(): void
587
    {
588 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
589 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
590
            try {
591 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
592 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $this->data['path']));
593 1
                if ($this->optimize) {
594 1
                    $this->optimize($filepath);
595
                }
596
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
597
                if (!$this->ignore_missing) {
598
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
599
                }
600
            }
601
        }
602
    }
603
604
    /**
605
     * Load file data.
606
     *
607
     * @throws RuntimeException
608
     */
609 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
610
    {
611 1
        $file = [];
612
613 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
614 1
            if ($ignore_missing) {
615 1
                $file['path'] = $path;
616 1
                $file['missing'] = true;
617
618 1
                return $file;
619
            }
620
621 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
622
        }
623
624 1
        if (Util\Url::isUrl($path)) {
625 1
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
626 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
627 1
            $force_slash = true;
628
        }
629 1
        if ($force_slash) {
630 1
            $path = '/' . ltrim($path, '/');
631
        }
632
633 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
634 1
        $content = Util\File::fileGetContents($filePath);
635
636 1
        $file['filepath'] = $filePath;
637 1
        $file['path'] = $path;
638 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
639 1
        $file['type'] = $type;
640 1
        $file['subtype'] = $subtype;
641 1
        $file['size'] = filesize($filePath);
642 1
        $file['content'] = $content;
643 1
        $file['missing'] = false;
644
645 1
        return $file;
646
    }
647
648
    /**
649
     * Try to find the file:
650
     *   1. remote (if $path is a valid URL)
651
     *   2. in static/
652
     *   3. in themes/<theme>/static/
653
     * Returns local file path or false if file don't exists.
654
     *
655
     * @throws RuntimeException
656
     *
657
     * @return string|false
658
     */
659 1
    private function findFile(string $path, ?string $remote_fallback = null)
660
    {
661
        // in case of remote file: save it and returns cached file path
662 1
        if (Util\Url::isUrl($path)) {
663 1
            $url = $path;
664 1
            $urlHost = parse_url($path, PHP_URL_HOST);
665 1
            $urlPath = parse_url($path, PHP_URL_PATH);
666 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
667 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
668
            // Google Fonts hack
669 1
            if (strpos($urlPath, '/css') !== false) {
670 1
                $extension = 'css';
671
            }
672 1
            $relativePath = Page::slugify(\sprintf(
673 1
                '%s%s%s%s',
674 1
                $urlHost,
675 1
                $this->sanitize($urlPath),
676 1
                $urlQuery ? "-$urlQuery" : '',
677 1
                $urlQuery && $extension ? " . $extension" : ''
678 1
            ));
679 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
680 1
            if (!file_exists($filePath)) {
681 1
                if (!Util\Url::isRemoteFileExists($url)) {
682
                    // is there a fallback in assets/
683 1
                    if ($remote_fallback) {
684 1
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
685 1
                        if (Util\File::getFS()->exists($filePath)) {
686
                            return $filePath;
687
                        }
688
                    }
689
690 1
                    return false;
691
                }
692 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
693
                    return false;
694
                }
695 1
                if (strlen($content) <= 1) {
696
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
697
                }
698 1
                Util\File::getFS()->dumpFile($filePath, $content);
699
            }
700
701 1
            return $filePath;
702
        }
703
704
        // checks in assets/
705 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
706 1
        if (Util\File::getFS()->exists($filePath)) {
707 1
            return $filePath;
708
        }
709
710
        // checks in each themes/<theme>/assets/
711 1
        foreach ($this->config->getTheme() as $theme) {
712 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
713 1
            if (Util\File::getFS()->exists($filePath)) {
714
                return $filePath;
715
            }
716
        }
717
718
        // checks in static/
719 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
720 1
        if (Util\File::getFS()->exists($filePath)) {
721 1
            return $filePath;
722
        }
723
724
        // checks in each themes/<theme>/static/
725 1
        foreach ($this->config->getTheme() as $theme) {
726 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
727 1
            if (Util\File::getFS()->exists($filePath)) {
728 1
                return $filePath;
729
            }
730
        }
731
732 1
        return false;
733
    }
734
735
    /**
736
     * Returns the width of an image/SVG.
737
     *
738
     * @throws RuntimeException
739
     */
740 1
    private function getWidth(): int
741
    {
742 1
        if ($this->data['type'] != 'image') {
743
            return 0;
744
        }
745 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
746 1
            return (int) $svg->width;
747
        }
748 1
        if (false === $size = $this->getImageSize()) {
749
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
750
        }
751
752 1
        return $size[0];
753
    }
754
755
    /**
756
     * Returns the height of an image/SVG.
757
     *
758
     * @throws RuntimeException
759
     */
760 1
    private function getHeight(): int
761
    {
762 1
        if ($this->data['type'] != 'image') {
763
            return 0;
764
        }
765 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
766 1
            return (int) $svg->height;
767
        }
768 1
        if (false === $size = $this->getImageSize()) {
769
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
770
        }
771
772 1
        return $size[1];
773
    }
774
775
    /**
776
     * Returns image size informations.
777
     *
778
     * @see https://www.php.net/manual/function.getimagesize.php
779
     *
780
     * @return array|false
781
     */
782 1
    private function getImageSize()
783
    {
784 1
        if (!$this->data['type'] == 'image') {
785
            return false;
786
        }
787
788
        try {
789 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
790 1
                return false;
791
            }
792
        } catch (\Exception $e) {
793
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
794
        }
795
796 1
        return $size;
797
    }
798
799
    /**
800
     * Returns true if asset is a SVG.
801
     */
802 1
    private function isSVG(): bool
803
    {
804 1
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
805
    }
806
807
    /**
808
     * Returns SVG attributes.
809
     *
810
     * @return \SimpleXMLElement|false
811
     */
812 1
    private function getSvgAttributes()
813
    {
814 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
815
            return false;
816
        }
817
818 1
        return $xml->attributes();
819
    }
820
821
    /**
822
     * Replaces some characters by '_'.
823
     */
824 1
    private function sanitize(string $string): string
825
    {
826 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
827
    }
828
}
829