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

src/Task/Assets/Minify.php (2 issues)

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;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
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;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
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)) {
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