Completed
Push — master ( a97913...36018d )
by Revin
03:44
created

View   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 485
Duplicated Lines 6.6 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 91.03%

Importance

Changes 44
Bugs 11 Features 6
Metric Value
wmc 72
c 44
b 11
f 6
lcom 1
cbo 9
dl 32
loc 485
ccs 213
cts 234
cp 0.9103
rs 5.5667

20 Methods

Rating   Name   Duplication   Size   Complexity  
C init() 6 33 8
B endPage() 0 30 5
B minifyCSS() 13 32 6
C processMinifyCss() 0 64 9
D minifyJS() 13 41 9
B processMinifyJs() 0 26 4
A isUrl() 0 9 2
A isContainsConditionalComment() 0 4 1
A thisFileNeedMinify() 0 4 2
A collectCharsets() 0 6 1
A collectImports() 0 6 1
A collectFonts() 0 6 1
A removeCssComments() 0 6 2
A removeJsComments() 0 7 2
B expandImports() 0 17 6
A _getImportContent() 0 18 4
A _collect() 0 14 2
A cleanFileName() 0 4 2
A getAbsoluteFilePath() 0 4 1
A _getSummaryFilesHash() 0 13 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like View often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use View, and based on these observations, apply Extract Interface, too.

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
    /**
62
     * @throws \rmrevin\yii\minify\Exception
63
     */
64 6
    public function init()
65
    {
66 6
        parent::init();
67
68 6
        $minify_path = $this->minify_path = (string)\Yii::getAlias($this->minify_path);
69 6
        if (!file_exists($minify_path)) {
70 6
            helpers\FileHelper::createDirectory($minify_path);
71 6
        }
72
73 6
        if (!is_readable($minify_path)) {
74
            throw new Exception('Directory for compressed assets is not readable.');
75
        }
76
77 6
        if (!is_writable($minify_path)) {
78
            throw new Exception('Directory for compressed assets is not writable.');
79
        }
80
81 6
        if (true === $this->compress_output) {
82
            \Yii::$app->response->on(\yii\web\Response::EVENT_BEFORE_SEND, function (\yii\base\Event $Event) {
83
                /** @var \yii\web\Response $Response */
84
                $Response = $Event->sender;
85
                if ($Response->format === \yii\web\Response::FORMAT_HTML) {
86 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...
87
                        $Response->data = HtmlCompressor::compress($Response->data, ['extra' => true]);
88
                    }
89
90 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...
91
                        $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...
92
                    }
93
                }
94
            });
95
        }
96 6
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101 4
    public function endPage($ajaxMode = false)
102
    {
103 4
        $this->trigger(self::EVENT_END_PAGE);
104
105 4
        $content = ob_get_clean();
106 4
        foreach (array_keys($this->assetBundles) as $bundle) {
107 4
            $this->registerAssetFiles($bundle);
108 4
        }
109
110 4
        if (true === $this->enableMinify) {
111 4
            if (true === $this->minifyCss) {
112 4
                $this->minifyCSS();
113 4
            }
114
115 4
            if (true === $this->minifyJs) {
116 4
                $this->minifyJS();
117 4
            }
118 4
        }
119
120 4
        echo strtr(
121 4
            $content,
122
            [
123 4
                self::PH_HEAD => $this->renderHeadHtml(),
124 4
                self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
125 4
                self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
126
            ]
127 4
        );
128
129 4
        $this->clear();
130 4
    }
131
132
    /**
133
     * @return self
134
     */
135 4
    protected function minifyCSS()
136
    {
137 4
        if (!empty($this->cssFiles)) {
138 4
            $cssFiles = $this->cssFiles;
139
140 4
            $this->cssFiles = [];
141
142 4
            $toMinify = [];
143
144 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...
145 4
                if ($this->thisFileNeedMinify($file, $html)) {
146 4
                    $toMinify[$file] = $html;
147 4
                } else {
148
                    if (!empty($toMinify)) {
149
                        $this->processMinifyCss($toMinify);
150
151
                        $toMinify = [];
152
                    }
153
154
                    $this->cssFiles[$file] = $html;
155
                }
156 4
            }
157
158 4
            if (!empty($toMinify)) {
159 4
                $this->processMinifyCss($toMinify);
160 4
            }
161
162 4
            unset($toMinify);
163 4
        }
164
165 4
        return $this;
166
    }
167
168
    /**
169
     * @param array $files
170
     */
171 4
    protected function processMinifyCss($files)
172
    {
173 4
        $resultFile = $this->minify_path . '/' . $this->_getSummaryFilesHash($files) . '.css';
174
175 4
        if (!file_exists($resultFile)) {
176 4
            $css = '';
177
178 4
            foreach ($files as $file => $html) {
179 4
                $path = dirname($file);
180 4
                $file = $this->getAbsoluteFilePath($file);
181
182 4
                $content = file_get_contents($file);
183
184 4
                preg_match_all('|url\(([^)]+)\)|is', $content, $m);
185 4
                if (!empty($m[0])) {
186 4
                    $result = [];
187
188 4
                    foreach ($m[0] as $k => $v) {
189 4
                        if (in_array(strpos($m[1][$k], 'data:'), [0, 1], true)) {
190 4
                            continue;
191
                        }
192
193 4
                        $url = str_replace(['\'', '"'], '', $m[1][$k]);
194
195 4
                        if ($this->isUrl($url)) {
196 4
                            $result[$m[1][$k]] = '\'' . $url . '\'';
197 4
                        } else {
198 4
                            $result[$m[1][$k]] = '\'' . $path . '/' . $url . '\'';
199
                        }
200 4
                    }
201
202 4
                    $content = str_replace(array_keys($result), array_values($result), $content);
203 4
                }
204
205 4
                $css .= $content;
206 4
            }
207
208 4
            $this->expandImports($css);
209
210 4
            $this->removeCssComments($css);
211
212 4
            $css = (new \CSSmin())
213 4
                ->run($css, $this->css_linebreak_pos);
214
215 4
            if (false !== $this->force_charset) {
216 2
                $charsets = '@charset "' . (string)$this->force_charset . '";' . "\n";
217 2
            } else {
218 2
                $charsets = $this->collectCharsets($css);
219
            }
220
221 4
            $imports = $this->collectImports($css);
222 4
            $fonts = $this->collectFonts($css);
223
224 4
            file_put_contents($resultFile, $charsets . $imports . $fonts . $css);
225
226 4
            if (false !== $this->file_mode) {
227 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...
228 4
            }
229 4
        }
230
231 4
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
232
233 4
        $this->cssFiles[$file] = helpers\Html::cssFile($file);
234 4
    }
235
236
    /**
237
     * @return self
238
     */
239 4
    protected function minifyJS()
240
    {
241 4
        if (!empty($this->jsFiles)) {
242 4
            $jsFiles = $this->jsFiles;
243
244 4
            foreach ($jsFiles as $position => $files) {
245 4
                if (false === in_array($position, $this->js_position, true)) {
246 4
                    $this->jsFiles[$position] = [];
247 4
                    foreach ($files as $file => $html) {
248 4
                        $this->jsFiles[$position][$file] = $html;
249 4
                    }
250 4
                } else {
251 4
                    $this->jsFiles[$position] = [];
252
253 4
                    $toMinify = [];
254
255 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...
256 4
                        if ($this->thisFileNeedMinify($file, $html)) {
257 4
                            $toMinify[$file] = $html;
258 4
                        } else {
259 4
                            if (!empty($toMinify)) {
260
                                $this->processMinifyJs($position, $toMinify);
261
262
                                $toMinify = [];
263
                            }
264
265 4
                            $this->jsFiles[$position][$file] = $html;
266
                        }
267 4
                    }
268
269 4
                    if (!empty($toMinify)) {
270 4
                        $this->processMinifyJs($position, $toMinify);
271 4
                    }
272
273 4
                    unset($toMinify);
274
                }
275 4
            }
276 4
        }
277
278 4
        return $this;
279
    }
280
281
    /**
282
     * @param integer $position
283
     * @param array $files
284
     */
285 4
    protected function processMinifyJs($position, $files)
286
    {
287 4
        $resultFile = sprintf('%s/%s.js', $this->minify_path, $this->_getSummaryFilesHash($files));
288 4
        if (!file_exists($resultFile)) {
289 4
            $js = '';
290 4
            foreach ($files as $file => $html) {
291 4
                $file = $this->getAbsoluteFilePath($file);
292 4
                $js .= file_get_contents($file) . ';' . PHP_EOL;
293 4
            }
294
295 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...
296
297 4
            $compressedJs = (new \JSMin($js))
298 4
                ->min();
299
300 4
            file_put_contents($resultFile, $compressedJs);
301
302 4
            if (false !== $this->file_mode) {
303 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...
304 4
            }
305 4
        }
306
307 4
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
308
309 4
        $this->jsFiles[$position][$file] = helpers\Html::jsFile($file);
310 4
    }
311
312
    /**
313
     * @param string $url
314
     * @param boolean $checkSlash
315
     * @return bool
316
     */
317 4
    protected function isUrl($url, $checkSlash = true)
318
    {
319 4
        $regexp = '#^(' . implode('|', $this->schemas) . ')#is';
320 4
        if ($checkSlash) {
321 4
            $regexp = '#^(/|\\\\|' . implode('|', $this->schemas) . ')#is';
322 4
        }
323
324 4
        return (bool)preg_match($regexp, $url);
325
    }
326
327
    /**
328
     * @param string $string
329
     * @return bool
330
     */
331 4
    protected function isContainsConditionalComment($string)
332
    {
333 4
        return strpos($string, '<![endif]-->') !== false;
334
    }
335
336
    /**
337
     * @param string $file
338
     * @param string $html
339
     * @return bool
340
     */
341 4
    protected function thisFileNeedMinify($file, $html)
342
    {
343 4
        return !$this->isUrl($file, false) && !$this->isContainsConditionalComment($html);
344
    }
345
346
    /**
347
     * @param string $code
348
     * @return string
349
     */
350 2
    protected function collectCharsets(&$code)
351
    {
352
        return $this->_collect($code, '|\@charset[^;]+|is', function ($string) {
353 2
            return $string . ';';
354 2
        });
355
    }
356
357
    /**
358
     * @param string $code
359
     * @return string
360
     */
361 4
    protected function collectImports(&$code)
362
    {
363
        return $this->_collect($code, '|\@import[^;]+|is', function ($string) {
364 2
            return $string . ';';
365 4
        });
366
    }
367
368
    /**
369
     * @param string $code
370
     * @return string
371
     */
372
    protected function collectFonts(&$code)
373
    {
374 4
        return $this->_collect($code, '|\@font-face\{[^}]+\}|is', function ($string) {
375 2
            return $string;
376 4
        });
377
    }
378
379
    /**
380
     * @param string $code
381
     */
382 4
    protected function removeCssComments(&$code)
383
    {
384 4
        if (true === $this->removeComments) {
385 4
            $code = preg_replace('#/\*(?:[^*]*(?:\*(?!/))*)*\*/#', '', $code);
386 4
        }
387 4
    }
388
389
    /**
390
     * @param string $code
391
     */
392 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...
393
    {
394
        // @todo
395 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...
396
            //$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...
397 4
        }
398 4
    }
399
400
    /**
401
     * @param string $code
402
     */
403 4
    protected function expandImports(&$code)
404
    {
405 4
        if (true === $this->expand_imports) {
406 2
            preg_match_all('|\@import\s([^;]+);|is', str_replace('&amp;', '&', $code), $m);
407 2
            if (!empty($m[0])) {
408 2
                foreach ($m[0] as $k => $v) {
409 2
                    $import_url = $m[1][$k];
410 2
                    if (!empty($import_url)) {
411 2
                        $import_content = $this->_getImportContent($import_url);
412 2
                        if (!empty($import_content)) {
413 2
                            $code = str_replace($m[0][$k], $import_content, $code);
414 2
                        }
415 2
                    }
416 2
                }
417 2
            }
418 2
        }
419 4
    }
420
421
    /**
422
     * @param string $url
423
     * @return null|string
424
     */
425 2
    protected function _getImportContent($url)
426
    {
427 2
        $result = null;
428
429 2
        if ('url(' === helpers\StringHelper::byteSubstr($url, 0, 4)) {
430 2
            $url = str_replace(['url(\'', 'url("', 'url(', '\')', '")', ')'], '', $url);
431
432 2
            if (helpers\StringHelper::byteSubstr($url, 0, 2) === '//') {
433 2
                $url = preg_replace('|^//|', 'http://', $url, 1);
434 2
            }
435
436 2
            if (!empty($url)) {
437 2
                $result = file_get_contents($url);
438 2
            }
439 2
        }
440
441 2
        return $result;
442
    }
443
444
    /**
445
     * @param string $code
446
     * @param string $pattern
447
     * @param callable $handler
448
     * @return string
449
     */
450 4
    protected function _collect(&$code, $pattern, $handler)
451
    {
452 4
        $result = '';
453
454 4
        preg_match_all($pattern, $code, $m);
455 4
        foreach ($m[0] as $string) {
456 4
            $string = $handler($string);
457 4
            $code = str_replace($string, '', $code);
458
459 4
            $result .= $string . PHP_EOL;
460 4
        }
461
462 4
        return $result;
463
    }
464
465
    /**
466
     * @param string $file
467
     * @return string
468
     */
469 4
    protected function cleanFileName($file)
470
    {
471 4
        return (strpos($file, '?')) ? parse_url($file, PHP_URL_PATH) : $file;
472
    }
473
474
    /**
475
     * @param string $file
476
     * @return string
477
     */
478 4
    protected function getAbsoluteFilePath($file)
479
    {
480 4
        return \Yii::getAlias($this->base_path) . str_replace(\Yii::getAlias($this->web_path), '', $this->cleanFileName($file));
481
    }
482
483
    /**
484
     * @param array $files
485
     * @return string
486
     */
487 4
    protected function _getSummaryFilesHash($files)
488
    {
489 4
        $result = '';
490 4
        foreach ($files as $file => $html) {
491 4
            $path = $this->getAbsoluteFilePath($file);
492
493 4
            if ($this->thisFileNeedMinify($file, $html) && file_exists($path)) {
494 4
                $result .= sha1_file($path);
495 4
            }
496 4
        }
497
498 4
        return sha1($result);
499
    }
500
}