Completed
Push — master ( 8b8afe...5f2bbe )
by Greg
02:21
created

src/Task/Assets/Minify.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace Robo\Task\Assets;
3
4
use Robo\Result;
5
use Robo\Task\BaseTask;
6
7
/**
8
 * Minifies asset file (CSS or JS).
9
 *
10
 * ``` php
11
 * <?php
12
 * $this->taskMinify( 'web/assets/theme.css' )
13
 *      ->run()
14
 * ?>
15
 * ```
16
 * Please install additional dependencies to use:
17
 *
18
 * ```
19
 * "patchwork/jsqueeze": "~1.0",
20
 * "natxet/CssMin": "~3.0"
21
 * ```
22
 */
23
class Minify extends BaseTask
24
{
25
    /**
26
     * @var array
27
     */
28
    protected $types = ['css', 'js'];
29
30
    /**
31
     * @var string
32
     */
33
    protected $text;
34
35
    /**
36
     * @var string
37
     */
38
    protected $dst;
39
40
    /**
41
     * @var string
42
     */
43
    protected $type;
44
45
    /**
46
     * @var array
47
     */
48
    protected $squeezeOptions = [
49
        'singleLine' => true,
50
        'keepImportantComments' => true,
51
        'specialVarRx' => false,
52
    ];
53
54
    /**
55
     * Constructor. Accepts asset file path or string source.
56
     *
57
     * @param string $input
58
     */
59
    public function __construct($input)
60
    {
61
        if (file_exists($input)) {
62
            $this->fromFile($input);
63
            return;
64
        }
65
66
        $this->fromText($input);
67
    }
68
69
    /**
70
     * Sets destination. Tries to guess type from it.
71
     *
72
     * @param string $dst
73
     *
74
     * @return $this
75
     */
76
    public function to($dst)
77
    {
78
        $this->dst = $dst;
79
80
        if (!empty($this->dst) && empty($this->type)) {
81
            $this->type($this->getExtension($this->dst));
82
        }
83
84
        return $this;
85
    }
86
87
    /**
88
     * Sets type with validation.
89
     *
90
     * @param string $type css|js
91
     *
92
     * @return $this
93
     */
94
    public function type($type)
95
    {
96
        $type = strtolower($type);
97
98
        if (in_array($type, $this->types)) {
99
            $this->type = $type;
100
        }
101
102
        return $this;
103
    }
104
105
    /**
106
     * Sets text from string source.
107
     *
108
     * @param string $text
109
     *
110
     * @return $this
111
     */
112
    protected function fromText($text)
113
    {
114
        $this->text = (string)$text;
115
        unset($this->type);
116
117
        return $this;
118
    }
119
120
    /**
121
     * Sets text from asset file path. Tries to guess type and set default destination.
122
     *
123
     * @param string $path
124
     *
125
     * @return $this
126
     */
127
    protected function fromFile($path)
128
    {
129
        $this->text = file_get_contents($path);
130
131
        unset($this->type);
132
        $this->type($this->getExtension($path));
133
134
        if (empty($this->dst) && !empty($this->type)) {
135
            $ext_length = strlen($this->type) + 1;
136
            $this->dst = substr($path, 0, -$ext_length) . '.min.' . $this->type;
137
        }
138
139
        return $this;
140
    }
141
142
    /**
143
     * Gets file extension from path.
144
     *
145
     * @param string $path
146
     *
147
     * @return string
148
     */
149
    protected function getExtension($path)
150
    {
151
        return pathinfo($path, PATHINFO_EXTENSION);
152
    }
153
154
    /**
155
     * Minifies and returns text.
156
     *
157
     * @return string|bool
158
     */
159
    protected function getMinifiedText()
160
    {
161
        switch ($this->type) {
162
            case 'css':
163
                if (!class_exists('\CssMin')) {
164
                    return Result::errorMissingPackage($this, 'CssMin', 'natxet/CssMin');
165
                }
166
167
                return \CssMin::minify($this->text);
168
                break;
169
170
            case 'js':
171
                if (!class_exists('\JSqueeze') && !class_exists('\Patchwork\JSqueeze')) {
172
                    return Result::errorMissingPackage($this, 'Patchwork\JSqueeze', 'patchwork/jsqueeze');
173
                }
174
175
                if (class_exists('\JSqueeze')) {
176
                    $jsqueeze = new \JSqueeze();
177
                } else {
178
                    $jsqueeze = new \Patchwork\JSqueeze();
179
                }
180
181
                return $jsqueeze->squeeze(
182
                    $this->text,
183
                    $this->squeezeOptions['singleLine'],
184
                    $this->squeezeOptions['keepImportantComments'],
185
                    $this->squeezeOptions['specialVarRx']
186
                );
187
                break;
188
        }
189
190
        return false;
191
    }
192
193
    /**
194
     * Single line option for the JS minimisation.
195
     *
196
     * @param bool $singleLine
197
     *
198
     * @return $this
199
     */
200
    public function singleLine($singleLine)
201
    {
202
        $this->squeezeOptions['singleLine'] = (bool)$singleLine;
203
        return $this;
204
    }
205
206
    /**
207
     * keepImportantComments option for the JS minimisation.
208
     *
209
     * @param bool $keepImportantComments
210
     *
211
     * @return $this
212
     */
213
    public function keepImportantComments($keepImportantComments)
214
    {
215
        $this->squeezeOptions['keepImportantComments'] = (bool)$keepImportantComments;
216
        return $this;
217
    }
218
219
    /**
220
     * specialVarRx option for the JS minimisation.
221
     *
222
     * @param bool $specialVarRx
223
     *
224
     * @return $this ;
225
     */
226
    public function specialVarRx($specialVarRx)
227
    {
228
        $this->squeezeOptions['specialVarRx'] = (bool)$specialVarRx;
229
        return $this;
230
    }
231
232
    /**
233
     * @return string
234
     */
235
    public function __toString()
236
    {
237
        return (string) $this->getMinifiedText();
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function run()
244
    {
245
        if (empty($this->type)) {
246
            return Result::error($this, 'Unknown asset type.');
247
        }
248
249
        if (empty($this->dst)) {
250
            return Result::error($this, 'Unknown file destination.');
251
        }
252
253 View Code Duplication
        if (file_exists($this->dst) && !is_writable($this->dst)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
254
            return Result::error($this, 'Destination already exists and cannot be overwritten.');
255
        }
256
257
        $size_before = strlen($this->text);
258
        $minified = $this->getMinifiedText();
259
260
        if ($minified instanceof Result) {
261
            return $minified;
262
        } elseif (false === $minified) {
263
            return Result::error($this, 'Minification failed.');
264
        }
265
266
        $size_after = strlen($minified);
267
268
        // Minification did not reduce file size, so use original file.
269
        if ($size_after > $size_before) {
270
            $minified = $this->text;
271
            $size_after = $size_before;
272
        }
273
274
        $dst = $this->dst . '.part';
275
        $write_result = file_put_contents($dst, $minified);
276
277
        if (false === $write_result) {
278
            @unlink($dst);
279
            return Result::error($this, 'File write failed.');
280
        }
281
        // Cannot be cross-volume; should always succeed.
282
        @rename($dst, $this->dst);
283
        if ($size_before === 0) {
284
            $minified_percent = 0;
285
        } else {
286
            $minified_percent = number_format(100 - ($size_after / $size_before * 100), 1);
287
        }
288
        $this->printTaskSuccess('Wrote {filepath}', ['filepath' => $this->dst]);
289
        $context = [
290
            'bytes' => $this->formatBytes($size_after),
291
            'reduction' => $this->formatBytes(($size_before - $size_after)),
292
            'percentage' => $minified_percent,
293
        ];
294
        $this->printTaskSuccess('Wrote {bytes} (reduced by {reduction} / {percentage})', $context);
295
        return Result::success($this, 'Asset minified.');
296
    }
297
}
298