Passed
Push — analysis-GD3Z1E ( 1a7ad2 )
by Arnaud
05:22 queued 15s
created

Asset::exists()   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 1
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
    /** @var Builder */
23
    protected $builder;
24
    /** @var Config */
25
    protected $config;
26
    /** @var string[] */
27
    protected $pathinfo = [];
28
    /** @var array */
29
    protected $data = [];
30
    /** @var bool */
31
    protected $compiled = false;
32
    /** @var bool */
33
    protected $versioned = 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
        $this->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'] = $path;
71
        $this->data['ext'] = $this->pathinfo['extension'];
72
        $this->data['type'] = explode('/', mime_content_type($filePath))[0];
73
        $this->data['content'] = file_get_contents($filePath);
74
        $this->data['attributes'] = $attributes;
75
76
        // compiling
77
        if ((bool) $this->config->get('assets.sass.auto')) {
78
            $this->compile();
79
        }
80
        // minifying
81
        if ($minify) {
82
            $this->minify();
83
        }
84
        // versionning
85
        if ($version) {
86
            $this->version();
87
        }
88
    }
89
90
    /**
91
     * Returns Asset path.
92
     *
93
     * @return string
94
     */
95
    public function __toString(): string
96
    {
97
        return $this->data['path'];
98
    }
99
100
    /**
101
     * Compiles a SCSS.
102
     *
103
     * @return self
104
     */
105
    public function compile(): self
106
    {
107
        if ($this->compiled) {
108
            return $this;
109
        }
110
111
        $data = $this->data;
112
113
        $this->data['path'] = preg_replace('/scss/m', 'css', $this->data['path']);
114
        $this->data['ext'] = 'css';
115
116
        if ($this->exists($this->data['path'])) {
117
            return $this;
118
        }
119
120
        if ($data['ext'] != 'scss') {
121
            return $this;
122
        }
123
124
        $cache = new Cache($this->builder, 'assets');
125
        $cacheKey = $cache->createKeyFromAsset($this);
126
        if (!$cache->has($cacheKey)) {
127
            $scssPhp = new Compiler();
128
            // import
129
            $scssDir = $this->config->get('assets.sass.dir') ?? [];
130
            $themes = $this->config->getTheme() ?? [];
131
            foreach ($scssDir as $dir) {
132
                $scssPhp->addImportPath(Util::joinPath($this->config->getStaticPath(), $dir));
133
                $scssPhp->addImportPath(Util::joinPath(dirname($data['file']), $dir));
134
                foreach ($themes as $theme) {
135
                    $scssPhp->addImportPath(Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir")));
136
                }
137
            }
138
            $scssPhp->setVariables($this->config->get('assets.sass.variables') ?? []);
139
            $scssPhp->setFormatter('ScssPhp\ScssPhp\Formatter\\'.ucfirst($this->config->get('assets.sass.style')));
140
            $this->data['content'] = $scssPhp->compile($data['content']);
141
            $cache->set($cacheKey, $this->data['content']);
142
        }
143
        $this->data['content'] = $cache->get($cacheKey, $this->data['content']);
144
145
        $this->save($data['path']);
146
147
        $this->compiled = true;
148
149
        return $this;
150
    }
151
152
    /**
153
     * Minifying a CSS or a JS.
154
     *
155
     * @return self
156
     */
157
    public function minify(): self
158
    {
159
        if ($this->minified) {
160
            return $this;
161
        }
162
163
        $data = $this->data;
164
165
        // dirname/filename.min.ext
166
        // ie: /styles.min.css
167
        $this->data['path'] = \sprintf(
168
            '%s.%s.%s',
169
            Util::joinPath($this->pathinfo['dirname'], $this->pathinfo['filename']),
170
            'min',
171
            $data['ext']
172
        );
173
        $this->data['ext'] = \sprintf('min.%s', $data['ext']);
174
175
        if ($this->exists($this->data['path'])) {
176
            return $this;
177
        }
178
179
        if ($data['ext'] == 'scss') {
180
            $this->compile();
181
        }
182
183
        if ($data['ext'] != 'css' && $data['ext'] != 'js') {
184
            return $this;
185
        }
186
187
        $cache = new Cache($this->builder, 'assets');
188
        $cacheKey = $cache->createKeyFromAsset($this);
189
        if (!$cache->has($cacheKey)) {
190
            switch ($data['ext']) {
191
                case 'css':
192
                    $minifier = new Minify\CSS($data['content']);
193
                    break;
194
                case 'js':
195
                    $minifier = new Minify\JS($data['content']);
196
                    break;
197
                default:
198
                    throw new Exception(sprintf('Not able to minify "%s"', $data['path']));
199
            }
200
            $this->data['content'] = $minifier->minify();
201
            $cache->set($cacheKey, $this->data['content']);
202
        }
203
        $this->data['content'] = $cache->get($cacheKey, $this->data['content']);
204
205
        $this->save($data['path']);
206
207
        $this->minified = true;
208
209
        return $this;
210
    }
211
212
    /**
213
     * Versions a file.
214
     *
215
     * @return self
216
     */
217
    public function version(): self
218
    {
219
        if ($this->versioned) {
220
            return $this;
221
        }
222
223
        $data = $this->data;
224
225
        $version = $this->builder->time;
226
        if ($this->config->get('assets.version.strategy') == 'static') {
227
            $version = $this->config->get('assets.version.value');
228
        }
229
230
        // dirname/filename.version.ext
231
        // ie: /styles.v1.css
232
        $this->data['path'] = \sprintf(
233
            '%s.%s.%s',
234
            Util::joinPath($this->pathinfo['dirname'], $this->pathinfo['filename']),
235
            $version,
236
            $this->data['ext']
237
        );
238
239
        if ($this->exists($this->data['path'])) {
240
            return $this;
241
        }
242
243
        $this->save($data['path']);
244
245
        $this->versioned = true;
246
247
        return $this;
248
    }
249
250
    /**
251
     * Implements \ArrayAccess.
252
     */
253
    public function offsetSet($offset, $value)
254
    {
255
        if (!is_null($offset)) {
256
            $this->data[$offset] = $value;
257
        }
258
    }
259
260
    /**
261
     * Implements \ArrayAccess.
262
     */
263
    public function offsetExists($offset)
264
    {
265
        return isset($this->data[$offset]);
266
    }
267
268
    /**
269
     * Implements \ArrayAccess.
270
     */
271
    public function offsetUnset($offset)
272
    {
273
        unset($this->data[$offset]);
274
    }
275
276
    /**
277
     * Implements \ArrayAccess.
278
     */
279
    public function offsetGet($offset)
280
    {
281
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
282
    }
283
284
    /**
285
     * Try to find a static file (in site or theme(s)) if exists or returns false.
286
     *
287
     * @param string $path
288
     *
289
     * @return string|false
290
     */
291
    private function findFile(string $path)
292
    {
293
        $filePath = Util::joinFile($this->config->getStaticPath(), $path);
294
        if (Util::getFS()->exists($filePath)) {
295
            return $filePath;
296
        }
297
298
        // checks in each theme
299
        foreach ($this->config->getTheme() as $theme) {
300
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
301
            if (Util::getFS()->exists($filePath)) {
302
                return $filePath;
303
            }
304
        }
305
306
        return false;
307
    }
308
309
    /**
310
     * Saves file (and deletes previous file).
311
     *
312
     * @param string $previousPath
313
     *
314
     * @return void
315
     */
316
    private function save(string $previousPath = null): void
317
    {
318
        if (!$this->builder->getBuildOptions()['dry-run']) {
319
            Util::getFS()->dumpFile(Util::joinFile($this->config->getOutputPath(), $this->data['path']), $this->data['content']);
320
            if (!empty($previousPath)) {
321
                Util::getFS()->remove(Util::joinFile($this->config->getOutputPath(), $previousPath));
322
            }
323
        }
324
    }
325
326
    /**
327
     * Checks if file is already saved/writen.
328
     *
329
     * @param string $path
330
     *
331
     * @return bool
332
     */
333
    private function exists(string $path): bool
334
    {
335
        return is_file(Util::joinFile($this->config->getOutputPath(), $path));
336
    }
337
}
338