Passed
Push — twig ( 3dc301...77b706 )
by Arnaud
02:26
created

Asset::getHash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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
20
class Asset implements \ArrayAccess
21
{
22
    const ASSETS_OUTPUT_DIR = '/';
23
24
    /** @var Builder */
25
    protected $builder;
26
    /** @var Config */
27
    protected $config;
28
    /** @var bool */
29
    /** @var array */
30
    protected $data = [];
31
    protected $versioned = false;
32
    /** @var bool */
33
    protected $compiled = false;
34
    /** @var bool */
35
    protected $minified = false;
36
37
    /**
38
     * Creates an Asset from file.
39
     *
40
     * $options[
41
     *     'minify'     => true,
42
     *     'version'    => true,
43
     *     'attributes' => ['title' => 'Titre'],
44
     * ];
45
     *
46
     * @param Builder    $builder
47
     * @param string     $path
48
     * @param array|null $options
49
     */
50
    public function __construct(Builder $builder, string $path, array $options = null)
51
    {
52
        $this->builder = $builder;
53
        $this->config = $builder->getConfig();
54
        $path = '/'.ltrim($path, '/');
55
56
        if (false === $filePath = $this->findFile($path)) {
57
            throw new Exception(sprintf('Asset file "%s" doesn\'t exist.', $path));
58
        }
59
60
        $pathinfo = pathinfo($path);
61
62
        // handles options
63
        $minify = (bool) $this->config->get('assets.minify.auto');
64
        $version = (bool) $this->config->get('assets.version.auto');
65
        $attributes = null;
66
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
67
68
        // set data
69
        $this->data['file'] = $filePath;
70
        $this->data['path'] = Util::joinPath(self::ASSETS_OUTPUT_DIR, $path);
71
        $this->data['ext'] = $pathinfo['extension'];
72
        $this->data['type'] = explode('/', mime_content_type($filePath))[0];
73
        $this->data['source'] = file_get_contents($filePath);
74
        $this->data['content'] = $this->data['source'];
75
        $this->data['attributes'] = $attributes;
76
77
        // versionning
78
        if ($version) {
79
            $this->version();
80
        }
81
        // compiling
82
        if ((bool) $this->config->get('assets.sass.auto')) {
83
            $this->compile();
84
        }
85
        // minifying
86
        if ($minify) {
87
            $this->minify();
88
        }
89
    }
90
91
    /**
92
     * Returns Asset path.
93
     *
94
     * @return string
95
     */
96
    public function __toString(): string
97
    {
98
        return $this->data['path'];
99
    }
100
101
    /**
102
     * Versions a file.
103
     *
104
     * @return self
105
     */
106
    public function version(): self
107
    {
108
        if ($this->versioned) {
109
            return $this;
110
        }
111
112
        switch ($this->config->get('assets.version.strategy')) {
113
            case 'static':
114
                $version = $this->config->get('assets.version.value');
115
                break;
116
            case 'buildtime':
117
                $version = $this->builder->time;
118
                break;
119
            case 'today':
120
            default:
121
                $version = date('Ymd');
122
                break;
123
        }
124
125
        if ($this->config->get('assets.version.strategy') == 'static') {
126
            $version = $this->config->get('assets.version.value');
127
        }
128
        $this->data['path'] = preg_replace(
129
            '/'.$this->data['ext'].'$/m',
130
            "$version.".$this->data['ext'],
131
            $this->data['path']
132
        );
133
134
        $this->versioned = true;
135
136
        return $this;
137
    }
138
139
    /**
140
     * Compiles a SCSS.
141
     *
142
     * @return self
143
     */
144
    public function compile(): self
145
    {
146
        if ($this->compiled) {
147
            return $this;
148
        }
149
150
        if ($this->data['ext'] != 'scss') {
151
            return $this;
152
        }
153
154
        $cache = new Cache($this->builder, 'assets');
155
        $cacheKey = $cache->createKeyFromAsset($this);
156
        if (!$cache->has($cacheKey)) {
157
            $scssPhp = new Compiler();
158
            // import
159
            $scssDir = $this->config->get('assets.sass.dir') ?? [];
160
            $themes = $this->config->getTheme() ?? [];
161
            foreach ($scssDir as $dir) {
162
                $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath(), $dir));
163
                $scssPhp->addImportPath(Util::joinPath(dirname($this->data['file']), $dir));
164
                foreach ($themes as $theme) {
165
                    $scssPhp->addImportPath(Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir")));
166
                }
167
            }
168
            $scssPhp->setVariables($this->config->get('assets.sass.variables') ?? []);
169
            $scssPhp->setFormatter('ScssPhp\ScssPhp\Formatter\\'.ucfirst($this->config->get('assets.sass.style')));
170
            $this->data['path'] = preg_replace('/scss/m', 'css', $this->data['path']);
171
            $this->data['ext'] = 'css';
172
            $this->data['content'] = $scssPhp->compile($this->data['content']);
173
            $this->compiled = true;
174
            $cache->set($cacheKey, $this->data);
175
        }
176
        $this->data = $cache->get($cacheKey);
177
178
        return $this;
179
    }
180
181
    /**
182
     * Minifying a CSS or a JS.
183
     *
184
     * @return self
185
     */
186
    public function minify(): self
187
    {
188
        if ($this->minified) {
189
            return $this;
190
        }
191
192
        if ($this->data['ext'] == 'scss') {
193
            $this->compile();
194
        }
195
196
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
197
            return $this;
198
        }
199
200
        $cache = new Cache($this->builder, 'assets');
201
        $cacheKey = $cache->createKeyFromAsset($this);
202
        if (!$cache->has($cacheKey)) {
203
            switch ($this->data['ext']) {
204
                case 'css':
205
                    $minifier = new Minify\CSS($this->data['content']);
206
                    break;
207
                case 'js':
208
                    $minifier = new Minify\JS($this->data['content']);
209
                    break;
210
                default:
211
                    throw new Exception(sprintf('Not able to minify "%s"', $this->data['path']));
212
            }
213
            $this->data['content'] = $minifier->minify();
214
            $this->minified = true;
215
            $cache->set($cacheKey, $this->data);
216
        }
217
        $this->data = $cache->get($cacheKey);
218
219
        return $this;
220
    }
221
222
    /**
223
     * Implements \ArrayAccess.
224
     */
225
    public function offsetSet($offset, $value)
226
    {
227
        if (!is_null($offset)) {
228
            $this->data[$offset] = $value;
229
        }
230
    }
231
232
    /**
233
     * Implements \ArrayAccess.
234
     */
235
    public function offsetExists($offset)
236
    {
237
        return isset($this->data[$offset]);
238
    }
239
240
    /**
241
     * Implements \ArrayAccess.
242
     */
243
    public function offsetUnset($offset)
244
    {
245
        unset($this->data[$offset]);
246
    }
247
248
    /**
249
     * Implements \ArrayAccess.
250
     */
251
    public function offsetGet($offset)
252
    {
253
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
254
    }
255
256
    /**
257
     * Hashing an asset with sha384.
258
     * Useful for SRI (Subresource Integrity).
259
     *
260
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
261
     *
262
     * @return string
263
     */
264
    public function getHash(): string
265
    {
266
        return sprintf('sha384-%s', base64_encode(hash('sha384', $this->data['content'], true)));
267
    }
268
269
    /**
270
     * Saves file.
271
     * Note: a file from `static/` with the same name will be overridden.
272
     *
273
     * @throws Exception
274
     *
275
     * @return void
276
     */
277
    public function save(): void
278
    {
279
        $file = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
280
        if (!$this->builder->getBuildOptions()['dry-run']) {
281
            try {
282
                Util::getFS()->dumpFile($file, $this->data['content']);
283
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
284
                throw new Exception(\sprintf('Can\'t save asset "%s"', $this->data['path']));
285
            }
286
        }
287
    }
288
289
    /**
290
     * Try to find a static file (in site or theme(s)) if exists or returns false.
291
     *
292
     * @param string $path
293
     *
294
     * @return string|false
295
     */
296
    private function findFile(string $path)
297
    {
298
        $filePath = Util::joinFile($this->config->getStaticPath(), $path);
299
        if (Util::getFS()->exists($filePath)) {
300
            return $filePath;
301
        }
302
303
        // checks in each theme
304
        foreach ($this->config->getTheme() as $theme) {
305
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
306
            if (Util::getFS()->exists($filePath)) {
307
                return $filePath;
308
            }
309
        }
310
311
        return false;
312
    }
313
}
314