Completed
Push — master ( 314766...b6ece2 )
by Greg
02:28
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');
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Robo\Result::err...Min', 'natxet/CssMin'); (Robo\Result) is incompatible with the return type documented by Robo\Task\Assets\Minify::getMinifiedText of type string|boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

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