Passed
Push — analysis-YjZ7v0 ( d474fa )
by Arnaud
05:14
created

Asset::__construct()   F

Complexity

Conditions 17
Paths 250

Size

Total Lines 73
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
cc 17
eloc 47
c 7
b 1
f 0
nc 250
nop 3
dl 0
loc 73
rs 3.7583

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
36
    /**
37
     * Creates an Asset from file.
38
     *
39
     * $options[
40
     *     'fingerprint' => false,
41
     *     'minify'      => false,
42
     * ];
43
     *
44
     * @param Builder      $builder
45
     * @param string|array $path
46
     * @param array|null   $options
47
     */
48
    public function __construct(Builder $builder, $path, array $options = null)
49
    {
50
        $this->builder = $builder;
51
        $this->config = $builder->getConfig();
52
        $path = is_array($path) ? $path : [$path];
53
54
        // handles options
55
        $ignore_missing = false;
56
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
57
        $minify = (bool) $this->config->get('assets.minify.enabled');
58
        $filename = '';
59
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
60
61
        // load file(s)
62
        $prevType = '';
63
        $prevExt = '';
64
        foreach ($path as $p) {
65
            $file = $this->loadFile($p, $ignore_missing);
66
            if (empty($file)) {
67
                continue;
68
            }
69
70
            // bundle: same type only
71
            if (!empty($prevType) && $file['type'] != $prevType) {
72
                throw new Exception(\sprintf('Asset bundle type error (%s != %s).', $file['type'], $prevType));
73
            }
74
            // bundle: same extension only
75
            if (!empty($prevExt) && $file['ext'] != $prevExt) {
76
                throw new Exception(\sprintf('Asset bundle extension error (%s != %s).', $file['ext'], $prevExt));
77
            }
78
79
            // set data
80
            $this->data['file'] = $file['filepath'];
81
            $this->data['path'] = $file['path'];
82
            $this->data['ext'] = $file['ext'];
83
            $this->data['type'] = $file['type'];
84
            $this->data['subtype'] = $file['subtype'];
85
            $this->data['size'] = $file['size'];
86
            $this->data['source'] = $file['content'];
87
            $this->data['content'] .= $file['content'];
88
89
            $prevType = $file['type'];
90
            $prevExt = $file['ext'];
91
        }
92
        // bunde: define path
93
        if (count($path) > 1) {
94
            $this->data['path'] = $filename;
95
            if (empty($filename)) {
96
                switch ($this->data['ext']) {
97
                    case 'scss':
98
                    case 'css':
99
                        $this->data['path'] = 'styles.'.$file['ext'];
100
                        break;
101
                    case 'js':
102
                        $this->data['path'] = 'scripts.'.$file['ext'];
103
                        break;
104
                    default:
105
                        throw new Exception(\sprintf('Asset bundle supports "%s" files only.', 'scss, css and js'));
106
                }
107
            }
108
        }
109
110
        // fingerprinting
111
        if ($fingerprint) {
112
            $this->fingerprint();
113
        }
114
        // compiling
115
        if ((bool) $this->config->get('assets.compile.enabled')) {
116
            $this->compile();
117
        }
118
        // minifying
119
        if ($minify) {
120
            $this->minify();
121
        }
122
    }
123
124
    /**
125
     * Returns Asset path.
126
     *
127
     * @return string
128
     */
129
    public function __toString(): string
130
    {
131
        return $this->data['path'];
132
    }
133
134
    /**
135
     * Fingerprints a file.
136
     *
137
     * @return self
138
     */
139
    public function fingerprint(): self
140
    {
141
        if ($this->fingerprinted) {
142
            return $this;
143
        }
144
145
        $fingerprint = hash('md5', $this->data['source']);
146
        $this->data['path'] = preg_replace(
147
            '/\.'.$this->data['ext'].'$/m',
148
            ".$fingerprint.".$this->data['ext'],
149
            $this->data['path']
150
        );
151
152
        $this->fingerprinted = true;
153
154
        return $this;
155
    }
156
157
    /**
158
     * Compiles a SCSS.
159
     *
160
     * @return self
161
     */
162
    public function compile(): self
163
    {
164
        if ($this->compiled) {
165
            return $this;
166
        }
167
168
        if ($this->data['ext'] != 'scss') {
169
            return $this;
170
        }
171
172
        $cache = new Cache($this->builder, 'assets');
173
        $cacheKey = $cache->createKeyFromAsset($this);
174
        if (!$cache->has($cacheKey)) {
175
            $scssPhp = new Compiler();
176
            // import path
177
            $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath()));
178
            $scssDir = $this->config->get('assets.compile.import') ?? [];
179
            $themes = $this->config->getTheme() ?? [];
180
            foreach ($scssDir as $dir) {
181
                $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath(), $dir));
182
                $scssPhp->addImportPath(Util::joinPath(dirname($this->data['file']), $dir));
183
                foreach ($themes as $theme) {
184
                    $scssPhp->addImportPath(Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir")));
185
                }
186
            }
187
            // output style
188
            $outputStyles = ['expanded', 'compressed'];
189
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
190
            if (!in_array($outputStyle, $outputStyles)) {
191
                throw new Exception(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
192
            }
193
            $scssPhp->setOutputStyle($outputStyle);
194
            // variables
195
            $scssPhp->setVariables($this->config->get('assets.compile.variables') ?? []);
196
            // update data
197
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
198
            $this->data['ext'] = 'css';
199
            $this->data['content'] = $scssPhp->compile($this->data['content']);
200
            $this->compiled = true;
201
            $cache->set($cacheKey, $this->data);
202
        }
203
        $this->data = $cache->get($cacheKey);
204
205
        return $this;
206
    }
207
208
    /**
209
     * Minifying a CSS or a JS.
210
     *
211
     * @return self
212
     */
213
    public function minify(): self
214
    {
215
        if ($this->minified) {
216
            return $this;
217
        }
218
219
        if ($this->data['ext'] == 'scss') {
220
            $this->compile();
221
        }
222
223
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
224
            return $this;
225
        }
226
227
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
228
            $this->minified;
229
230
            return $this;
231
        }
232
233
        $cache = new Cache($this->builder, 'assets');
234
        $cacheKey = $cache->createKeyFromAsset($this);
235
        if (!$cache->has($cacheKey)) {
236
            switch ($this->data['ext']) {
237
                case 'css':
238
                    $minifier = new Minify\CSS($this->data['content']);
239
                    break;
240
                case 'js':
241
                    $minifier = new Minify\JS($this->data['content']);
242
                    break;
243
                default:
244
                    throw new Exception(sprintf('Not able to minify "%s"', $this->data['path']));
245
            }
246
            $this->data['path'] = preg_replace(
247
                '/\.'.$this->data['ext'].'$/m',
248
                '.min.'.$this->data['ext'],
249
                $this->data['path']
250
            );
251
            $this->data['content'] = $minifier->minify();
252
            $this->minified = true;
253
            $cache->set($cacheKey, $this->data);
254
        }
255
        $this->data = $cache->get($cacheKey);
256
257
        return $this;
258
    }
259
260
    /**
261
     * Implements \ArrayAccess.
262
     */
263
    public function offsetSet($offset, $value)
264
    {
265
        if (!is_null($offset)) {
266
            $this->data[$offset] = $value;
267
        }
268
    }
269
270
    /**
271
     * Implements \ArrayAccess.
272
     */
273
    public function offsetExists($offset)
274
    {
275
        return isset($this->data[$offset]);
276
    }
277
278
    /**
279
     * Implements \ArrayAccess.
280
     */
281
    public function offsetUnset($offset)
282
    {
283
        unset($this->data[$offset]);
284
    }
285
286
    /**
287
     * Implements \ArrayAccess.
288
     */
289
    public function offsetGet($offset)
290
    {
291
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
292
    }
293
294
    /**
295
     * Hashing content of an asset with the specified algo, sha384 by default.
296
     * Used for SRI (Subresource Integrity).
297
     *
298
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
299
     *
300
     * @return string
301
     */
302
    public function getIntegrity(string $algo = 'sha384'): string
303
    {
304
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
305
    }
306
307
    /**
308
     * Returns the width of an image.
309
     *
310
     * @return false|int
311
     */
312
    public function getWidth()
313
    {
314
        if (false === $size = $this->getImageSize()) {
315
            return false;
316
        }
317
318
        return $size[0];
319
    }
320
321
    /**
322
     * Returns the height of an image.
323
     *
324
     * @return false|int
325
     */
326
    public function getHeight()
327
    {
328
        if (false === $size = $this->getImageSize()) {
329
            return false;
330
        }
331
332
        return $size[1];
333
    }
334
335
    /**
336
     * Returns MP3 file infos.
337
     *
338
     * @see https://github.com/wapmorgan/Mp3Info
339
     *
340
     * @return Mp3Info
341
     */
342
    public function getAudio(): Mp3Info
343
    {
344
        return new Mp3Info($this->data['file']);
345
    }
346
347
    /**
348
     * Saves file.
349
     * Note: a file from `static/` with the same name will be overridden.
350
     *
351
     * @throws Exception
352
     *
353
     * @return void
354
     */
355
    public function save(): void
356
    {
357
        $file = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
358
        if (!$this->builder->getBuildOptions()['dry-run']) {
359
            try {
360
                Util::getFS()->dumpFile($file, $this->data['content']);
361
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
362
                throw new Exception(\sprintf('Can\'t save asset "%s"', $this->data['path']));
363
            }
364
        }
365
    }
366
367
    /**
368
     * Load file data.
369
     *
370
     * @param string $path
371
     * @param bool   $ignore_missing
372
     *
373
     * @return array
374
     */
375
    private function loadFile(string $path, bool $ignore_missing): array
376
    {
377
        $file = [];
378
        $path = '/'.ltrim($path, '/');
379
380
        if (false === $filePath = $this->findFile($path)) {
381
            if ($ignore_missing) {
382
                return [];
383
            }
384
385
            throw new Exception(sprintf('Asset file "%s" doesn\'t exist.', $path));
386
        }
387
388
        $pathinfo = pathinfo($path);
389
        list($type, $subtype) = Util::getMimeType($filePath);
390
        $content = file_get_contents($filePath);
391
392
        $file['filepath'] = $filePath;
393
        $file['path'] = $path;
394
        $file['ext'] = $pathinfo['extension'];
395
        $file['type'] = $type;
396
        $file['subtype'] = $subtype;
397
        $file['size'] = filesize($filePath);
398
        $file['content'] = $content;
399
400
        return $file;
401
    }
402
403
    /**
404
     * Try to find a static file (in site or theme(s)) if exists or returns false.
405
     *
406
     * @param string $path
407
     *
408
     * @return string|false
409
     */
410
    private function findFile(string $path)
411
    {
412
        $filePath = Util::joinFile($this->config->getStaticPath(), $path);
413
        if (Util::getFS()->exists($filePath)) {
414
            return $filePath;
415
        }
416
417
        // checks in each theme
418
        foreach ($this->config->getTheme() as $theme) {
419
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
420
            if (Util::getFS()->exists($filePath)) {
421
                return $filePath;
422
            }
423
        }
424
425
        return false;
426
    }
427
428
    /**
429
     * Returns image size informations.
430
     *
431
     * See https://www.php.net/manual/function.getimagesize.php
432
     *
433
     * @return false|array
434
     */
435
    private function getImageSize()
436
    {
437
        if (!$this->data['type'] == 'image') {
438
            return false;
439
        }
440
441
        if (false === $size = getimagesizefromstring($this->data['content'])) {
442
            return false;
443
        }
444
445
        return $size;
446
    }
447
}
448