Completed
Pull Request — master (#28)
by
unknown
10:12
created

View::init()   D

Complexity

Conditions 10
Paths 24

Size

Total Lines 40
Code Lines 21

Duplication

Lines 6
Ratio 15 %

Code Coverage

Tests 11
CRAP Score 33.9136

Importance

Changes 9
Bugs 0 Features 1
Metric Value
c 9
b 0
f 1
dl 6
loc 40
ccs 11
cts 29
cp 0.3793
rs 4.8196
cc 10
eloc 21
nc 24
nop 0
crap 33.9136

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * View.php
4
 * @author Revin Roman
5
 * @link https://rmrevin.ru
6
 */
7
8
namespace rmrevin\yii\minify;
9
10
use yii\helpers;
11
12
/**
13
 * Class View
14
 * @package rmrevin\yii\minify
15
 */
16
class View extends \yii\web\View
17
{
18
19
    /** @var bool */
20
    public $enableMinify = true;
21
22
    /** @var bool */
23
    public $minifyCss = true;
24
25
    /** @var bool */
26
    public $minifyJs = true;
27
28
    /** @var bool */
29
    public $removeComments = true;
30
31
    /** @var string path alias to web base (in url) */
32
    public $web_path = '@web';
33
34
    /** @var string path alias to web base (absolute) */
35
    public $base_path = '@webroot';
36
37
    /** @var string path alias to save minify result */
38
    public $minify_path = '@webroot/minify';
39
40
    /** @var array positions of js files to be minified */
41
    public $js_position = [self::POS_END, self::POS_HEAD];
42
43
    /** @var bool|string charset forcibly assign, otherwise will use all of the files found charset */
44
    public $force_charset = false;
45
46
    /** @var bool whether to change @import on content */
47
    public $expand_imports = true;
48
49
    /** @var int */
50
    public $css_linebreak_pos = 2048;
51
52
    /** @var int|bool chmod of minified file. If false chmod not set */
53
    public $file_mode = 0664;
54
55
    /** @var array schemes that will be ignored during normalization url */
56
    public $schemas = ['//', 'http://', 'https://', 'ftp://'];
57
58
    /** @var bool do I need to compress the result html page. */
59
    public $compress_output = false;
60
61
    /** @var array routes to exclude from minify. */
62
    public $exclude_routes = [];
63
64
    /**
65
     * @throws \rmrevin\yii\minify\Exception
66
     */
67 6
    public function init()
68
    {
69 6
        parent::init();
70
71 6
        if (php_sapi_name() !== 'cli') {
72
            $urlDetails = \Yii::$app->urlManager->parseRequest(\Yii::$app->request);
73
            if (in_array($urlDetails[0], $this->exclude_routes)) {
74
                $this->enableMinify = false;
75
            }
76
        }
77
78 6
        $minify_path = $this->minify_path = (string)\Yii::getAlias($this->minify_path);
79 6
        if (!file_exists($minify_path)) {
80 6
            helpers\FileHelper::createDirectory($minify_path);
81 6
        }
82
83 6
        if (!is_readable($minify_path)) {
84
            throw new Exception('Directory for compressed assets is not readable.');
85
        }
86
87 6
        if (!is_writable($minify_path)) {
88
            throw new Exception('Directory for compressed assets is not writable.');
89
        }
90
91 6
        if (true === $this->compress_output) {
92
            \Yii::$app->response->on(\yii\web\Response::EVENT_BEFORE_SEND, function (\yii\base\Event $Event) {
93
                /** @var \yii\web\Response $Response */
94
                $Response = $Event->sender;
95
                if ($Response->format === \yii\web\Response::FORMAT_HTML) {
96 View Code Duplication
                    if (!empty($Response->data)) {
0 ignored issues
show
Duplication introduced by
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...
97
                        $Response->data = HtmlCompressor::compress($Response->data, ['extra' => true]);
98
                    }
99
100 View Code Duplication
                    if (!empty($Response->content)) {
0 ignored issues
show
Duplication introduced by
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...
101
                        $Response->content = HtmlCompressor::compress($Response->content, ['extra' => true]);
0 ignored issues
show
Documentation Bug introduced by
It seems like \rmrevin\yii\minify\Html...array('extra' => true)) can also be of type false. However, the property $content is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
102
                    }
103
                }
104
            });
105
        }
106 6
    }
107
108
    /**
109
     * @inheritdoc
110
     */
111 4
    public function endPage($ajaxMode = false)
112
    {
113 4
        $this->trigger(self::EVENT_END_PAGE);
114
115 4
        $content = ob_get_clean();
116 4
        foreach (array_keys($this->assetBundles) as $bundle) {
117 4
            $this->registerAssetFiles($bundle);
118 4
        }
119
120 4
        if (true === $this->enableMinify) {
121 4
            if (true === $this->minifyCss) {
122 4
                $this->minifyCSS();
123 4
            }
124
125 4
            if (true === $this->minifyJs) {
126 4
                $this->minifyJS();
127 4
            }
128 4
        }
129
130 4
        echo strtr(
131 4
            $content,
132
            [
133 4
                self::PH_HEAD => $this->renderHeadHtml(),
134 4
                self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
135 4
                self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
136
            ]
137 4
        );
138
139 4
        $this->clear();
140 4
    }
141
142
    /**
143
     * @return self
144
     */
145 4
    protected function minifyCSS()
146
    {
147 4
        if (!empty($this->cssFiles)) {
148 4
            $cssFiles = $this->cssFiles;
149
150 4
            $this->cssFiles = [];
151
152 4
            $toMinify = [];
153
154 4 View Code Duplication
            foreach ($cssFiles as $file => $html) {
0 ignored issues
show
Duplication introduced by
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...
155 4
                if ($this->thisFileNeedMinify($file, $html)) {
156 4
                    $toMinify[$file] = $html;
157 4
                } else {
158
                    if (!empty($toMinify)) {
159
                        $this->processMinifyCss($toMinify);
160
161
                        $toMinify = [];
162
                    }
163
164
                    $this->cssFiles[$file] = $html;
165
                }
166 4
            }
167
168 4
            if (!empty($toMinify)) {
169 4
                $this->processMinifyCss($toMinify);
170 4
            }
171
172 4
            unset($toMinify);
173 4
        }
174
175 4
        return $this;
176
    }
177
178
    /**
179
     * @param array $files
180
     */
181 4
    protected function processMinifyCss($files)
182
    {
183 4
        $resultFile = $this->minify_path . '/' . $this->_getSummaryFilesHash($files) . '.css';
184
185 4
        if (!file_exists($resultFile)) {
186 4
            $css = '';
187
188 4
            foreach ($files as $file => $html) {
189 4
                $file = (strpos($file, '?')) ? parse_url($file, PHP_URL_PATH) : $file;
190 4
                $file = str_replace(\Yii::getAlias($this->web_path), '', $file);
191
192 4
                $content = file_get_contents(\Yii::getAlias($this->base_path) . $file);
193
194 4
                preg_match_all('|url\(([^)]+)\)|is', $content, $m);
195 4
                if (!empty($m[0])) {
196 4
                    $path = dirname($file);
197 4
                    $result = [];
198 4
                    foreach ($m[0] as $k => $v) {
199 4
                        if (in_array(strpos($m[1][$k], 'data:'), [0, 1], true)) {
200 4
                            continue;
201
                        }
202 4
                        $url = str_replace(['\'', '"'], '', $m[1][$k]);
203 4
                        if ($this->isUrl($url)) {
204 4
                            $result[$m[1][$k]] = '\'' . $url . '\'';
205 4
                        } else {
206 4
                            $result[$m[1][$k]] = '\'' . \Yii::getAlias($this->web_path) . $path . '/' . $url . '\'';
207
                        }
208 4
                    }
209 4
                    $content = str_replace(array_keys($result), array_values($result), $content);
210 4
                }
211
212 4
                $css .= $content;
213 4
            }
214
215 4
            $this->expandImports($css);
216
217 4
            $this->removeCssComments($css);
218
219 4
            $css = (new \CSSmin())
220 4
                ->run($css, $this->css_linebreak_pos);
221
222 4
            if (false !== $this->force_charset) {
223 2
                $charsets = '@charset "' . (string)$this->force_charset . '";' . "\n";
224 2
            } else {
225 2
                $charsets = $this->collectCharsets($css);
226
            }
227
228 4
            $imports = $this->collectImports($css);
229 4
            $fonts = $this->collectFonts($css);
230
231 4
            file_put_contents($resultFile, $charsets . $imports . $fonts . $css);
232
233 4
            if (false !== $this->file_mode) {
234 4
                @chmod($resultFile, $this->file_mode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
235 4
            }
236 4
        }
237
238 4
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
239
240 4
        $this->cssFiles[$file] = helpers\Html::cssFile($file);
241 4
    }
242
243
    /**
244
     * @return self
245
     */
246 4
    protected function minifyJS()
247
    {
248 4
        if (!empty($this->jsFiles)) {
249 4
            $jsFiles = $this->jsFiles;
250
251 4
            foreach ($jsFiles as $position => $files) {
252 4
                if (false === in_array($position, $this->js_position, true)) {
253 4
                    $this->jsFiles[$position] = [];
254 4
                    foreach ($files as $file => $html) {
255 4
                        $this->jsFiles[$position][$file] = $html;
256 4
                    }
257 4
                } else {
258 4
                    $this->jsFiles[$position] = [];
259
260 4
                    $toMinify = [];
261
262 4 View Code Duplication
                    foreach ($files as $file => $html) {
0 ignored issues
show
Duplication introduced by
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...
263 4
                        if ($this->thisFileNeedMinify($file, $html)) {
264 4
                            $toMinify[$file] = $html;
265 4
                        } else {
266 4
                            if (!empty($toMinify)) {
267
                                $this->processMinifyJs($position, $toMinify);
268
269
                                $toMinify = [];
270
                            }
271
272 4
                            $this->jsFiles[$position][$file] = $html;
273
                        }
274 4
                    }
275
276 4
                    if (!empty($toMinify)) {
277 4
                        $this->processMinifyJs($position, $toMinify);
278 4
                    }
279
280 4
                    unset($toMinify);
281
                }
282 4
            }
283 4
        }
284
285 4
        return $this;
286
    }
287
288
    /**
289
     * @param integer $position
290
     * @param array $files
291
     */
292 4
    protected function processMinifyJs($position, $files)
293
    {
294 4
        $resultFile = sprintf('%s/%s.js', $this->minify_path, $this->_getSummaryFilesHash($files));
295 4
        if (!file_exists($resultFile)) {
296 4
            $js = '';
297 4
            foreach ($files as $file => $html) {
298 4
                $file = (strpos($file, '?')) ? parse_url($file, PHP_URL_PATH) : $file;
299 4
                $file = $this->getAbsoluteFilePath($file);
300 4
                $js .= file_get_contents($file) . ';' . PHP_EOL;
301 4
            }
302
303 4
            $this->removeJsComments($js);
0 ignored issues
show
Unused Code introduced by
The call to the method rmrevin\yii\minify\View::removeJsComments() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
304
305 4
            $compressedJs = (new \JSMin($js))
306 4
                ->min();
307
308 4
            file_put_contents($resultFile, $compressedJs);
309
310 4
            if (false !== $this->file_mode) {
311 4
                @chmod($resultFile, $this->file_mode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
312 4
            }
313 4
        }
314
315 4
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
316
317 4
        $this->jsFiles[$position][$file] = helpers\Html::jsFile($file);
318 4
    }
319
320
    /**
321
     * @param string $url
322
     * @param boolean $checkSlash
323
     * @return bool
324
     */
325 4
    protected function isUrl($url, $checkSlash = true)
326
    {
327 4
        $regexp = '#^(' . implode('|', $this->schemas) . ')#is';
328 4
        if ($checkSlash) {
329 4
            $regexp = '#^(/|\\\\|' . implode('|', $this->schemas) . ')#is';
330 4
        }
331
332 4
        return (bool)preg_match($regexp, $url);
333
    }
334
335
    /**
336
     * @param string $string
337
     * @return bool
338
     */
339 4
    protected function isContainsConditionalComment($string)
340
    {
341 4
        return strpos($string, '<![endif]-->') !== false;
342
    }
343
344
    /**
345
     * @param string $file
346
     * @param string $html
347
     * @return bool
348
     */
349 4
    protected function thisFileNeedMinify($file, $html)
350
    {
351 4
        return !$this->isUrl($file, false) && !$this->isContainsConditionalComment($html);
352
    }
353
354
    /**
355
     * @param string $code
356
     * @return string
357
     */
358 2
    protected function collectCharsets(&$code)
359
    {
360
        return $this->_collect($code, '|\@charset[^;]+|is', function ($string) {
361 2
            return $string . ';';
362 2
        });
363
    }
364
365
    /**
366
     * @param string $code
367
     * @return string
368
     */
369 4
    protected function collectImports(&$code)
370
    {
371
        return $this->_collect($code, '|\@import[^;]+|is', function ($string) {
372 2
            return $string . ';';
373 4
        });
374
    }
375
376
    /**
377
     * @param string $code
378
     * @return string
379
     */
380
    protected function collectFonts(&$code)
381
    {
382 4
        return $this->_collect($code, '|\@font-face\{[^}]+\}|is', function ($string) {
383 2
            return $string;
384 4
        });
385
    }
386
387
    /**
388
     * @param string $code
389
     */
390 4
    protected function removeCssComments(&$code)
391
    {
392 4
        if (true === $this->removeComments) {
393 4
            $code = preg_replace('#/\*(?:[^*]*(?:\*(?!/))*)*\*/#', '', $code);
394 4
        }
395 4
    }
396
397
    /**
398
     * @param string $code
399
     */
400 4
    protected function removeJsComments(&$code)
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
401
    {
402
        // @todo
403 4
        if (true === $this->removeComments) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
404
            //$code = preg_replace('', '', $code);
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
405 4
        }
406 4
    }
407
408
    /**
409
     * @param string $code
410
     */
411 4
    protected function expandImports(&$code)
412
    {
413 4
        if (true === $this->expand_imports) {
414 2
            preg_match_all('|\@import\s([^;]+);|is', str_replace('&amp;', '&', $code), $m);
415 2
            if (!empty($m[0])) {
416 2
                foreach ($m[0] as $k => $v) {
417 2
                    $import_url = $m[1][$k];
418 2
                    if (!empty($import_url)) {
419 2
                        $import_content = $this->_getImportContent($import_url);
420 2
                        if (!empty($import_content)) {
421 2
                            $code = str_replace($m[0][$k], $import_content, $code);
422 2
                        }
423 2
                    }
424 2
                }
425 2
            }
426 2
        }
427 4
    }
428
429
    /**
430
     * @param string $url
431
     * @return null|string
432
     */
433 2
    protected function _getImportContent($url)
434
    {
435 2
        $result = null;
436
437 2
        if ('url(' === helpers\StringHelper::byteSubstr($url, 0, 4)) {
438 2
            $url = str_replace(['url(\'', 'url("', 'url(', '\')', '")', ')'], '', $url);
439
440 2
            if (helpers\StringHelper::byteSubstr($url, 0, 2) === '//') {
441 2
                $url = preg_replace('|^//|', 'http://', $url, 1);
442 2
            }
443
444 2
            if (!empty($url)) {
445 2
                $result = file_get_contents($url);
446 2
            }
447 2
        }
448
449 2
        return $result;
450
    }
451
452
    /**
453
     * @param string $code
454
     * @param string $pattern
455
     * @param callable $handler
456
     * @return string
457
     */
458 4
    protected function _collect(&$code, $pattern, $handler)
459
    {
460 4
        $result = '';
461
462 4
        preg_match_all($pattern, $code, $m);
463 4
        foreach ($m[0] as $string) {
464 4
            $string = $handler($string);
465 4
            $code = str_replace($string, '', $code);
466
467 4
            $result .= $string . PHP_EOL;
468 4
        }
469
470 4
        return $result;
471
    }
472
473
    /**
474
     * @param string $file
475
     * @return string
476
     */
477 4
    protected function getAbsoluteFilePath($file)
478
    {
479 4
        return \Yii::getAlias($this->base_path) . str_replace(\Yii::getAlias($this->web_path), '', $file);
480
    }
481
482
    /**
483
     * @param array $files
484
     * @return string
485
     */
486 4
    protected function _getSummaryFilesHash($files)
487
    {
488 4
        $result = '';
489 4
        foreach ($files as $file => $html) {
490 4
            $path = $this->getAbsoluteFilePath($file);
491
492 4
            if ($this->thisFileNeedMinify($file, $html) && file_exists($path)) {
493 2
                $result .= sha1_file($path);
494 2
            }
495 4
        }
496
497 4
        return sha1($result);
498
    }
499
}