Completed
Push — master ( 2d4a78...296b87 )
by Craig
06:04
created

Merger   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 291
Duplicated Lines 8.25 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 24
loc 291
rs 8.3206
c 0
b 0
f 0
wmc 51
lcom 1
cbo 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
D merge() 0 43 9
D readFile() 24 128 32
B cssFixPath() 0 20 8
A minify() 0 11 1

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 Merger 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 Merger, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Zikula package.
5
 *
6
 * Copyright Zikula Foundation - http://zikula.org/
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zikula\ThemeModule\Engine\Asset;
13
14
use Doctrine\Common\Cache\CacheProvider;
15
use Symfony\Component\Routing\RouterInterface;
16
17
class Merger implements MergerInterface
18
{
19
    /**
20
     * @var RouterInterface
21
     */
22
    private $router;
23
24
    /**
25
     * @var CacheProvider
26
     */
27
    private $cssCache;
28
29
    /**
30
     * @var CacheProvider
31
     */
32
    private $jsCache;
33
34
    /**
35
     * @var string
36
     */
37
    private $rootDir;
38
39
    /**
40
     * @var integer
41
     */
42
    private $lifetime;
43
44
    /**
45
     * @var boolean
46
     */
47
    private $minify;
48
49
    /**
50
     * @var boolean
51
     */
52
    private $compress;
53
54
    /**
55
     * Merger constructor.
56
     * @param RouterInterface $router
57
     * @param CacheProvider $jsCache
58
     * @param CacheProvider $cssCache
59
     * @param string $kernelRootDir
60
     * @param string $lifetime
61
     * @param bool $minify
62
     * @param bool $compress
63
     */
64
    public function __construct(
65
        RouterInterface $router,
66
        CacheProvider $jsCache,
67
        CacheProvider $cssCache,
68
        $kernelRootDir,
69
        $lifetime = "1 day",
70
        $minify = false,
71
        $compress = false
72
    ) {
73
        $this->router = $router;
74
        $this->jsCache = $jsCache;
75
        $this->cssCache = $cssCache;
76
        $rootDir = realpath($kernelRootDir . '/../');
77
        $this->rootDir = str_replace($router->getContext()->getBaseUrl(), '', $rootDir);
78
        $this->lifetime = abs((new \DateTime($lifetime))->getTimestamp() - (new \DateTime())->getTimestamp());
0 ignored issues
show
Documentation Bug introduced by
It seems like abs((new \DateTime($life...ime())->getTimestamp()) can also be of type double. However, the property $lifetime is declared as type integer. 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...
79
        $this->minify = $minify;
80
        $this->compress = $compress;
81
    }
82
83
    public function merge(array $assets, $type = 'js')
84
    {
85
        $preCachedFiles = [];
86
        $cachedFiles = [];
87
        $outputFiles = [];
88
        // skip remote files from combining
89
        foreach ($assets as $weight => $asset) {
90
            $path = realpath($this->rootDir . $asset);
91
            if (is_file($path)) {
92
                $cachedFiles[] = $path;
93
            } else {
94
                if ($weight < 0) {
95
                    $preCachedFiles[] = $asset;
96
                } else {
97
                    $outputFiles[] = $asset;
98
                }
99
            }
100
        }
101
        $cacheName = in_array($type, ['js', 'css']) ? "{$type}Cache" : null;
102
        /** @var CacheProvider $cacheService */
103
        $cacheService = $this->$cacheName;
104
        $key = md5(serialize($assets)) . (int)$this->minify . (int)$this->compress . $this->lifetime . '.' . $type;
105
        $data = $cacheService->fetch($key);
106
        if ($data === false) {
107
            $data = [];
108
            foreach ($cachedFiles as $file) {
109
                $this->readFile($data, $file, $type);
110
            }
111
            $now = new \DateTime();
112
            array_unshift($data, sprintf("/* --- Combined file written: %s */\n\n", $now->format('c')));
113
            array_unshift($data, sprintf("/* --- Combined files:\n%s\n*/\n\n", implode("\n", $cachedFiles)));
114
            $data = implode('', $data);
115
            if ($type == 'css' && $this->minify) {
116
                $data = $this->minify($data);
117
            }
118
            $cacheService->save($key, $data, $this->lifetime);
119
        }
120
        $route = $this->router->generate('zikulathememodule_combinedasset_asset', ['type' => $type, 'key' => $key]);
121
        array_unshift($outputFiles, $route);
122
        array_unshift($outputFiles, $preCachedFiles);
123
124
        return $outputFiles;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $outputFiles; (array) is incompatible with the return type declared by the interface Zikula\ThemeModule\Engin...\MergerInterface::merge of type Zikula\ThemeModule\Engine\Asset\MergerInterface.

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...
125
    }
126
127
    /**
128
     * Read a file and add its contents to the $contents array.
129
     * This function includes the content of all "@import" statements (recursive).
130
     *
131
     * @param array &$contents Array to save content to
132
     * @param string $file Path to file
133
     * @param string $ext Can be 'css' or 'js'
134
     */
135
    private function readFile(&$contents, $file, $ext)
136
    {
137
        if (!file_exists($file)) {
138
            return;
139
        }
140
        $source = fopen($file, 'r');
141
        if (!$source) {
142
            return;
143
        }
144
145
        $contents[] = "/* --- Source file: {$file} */\n\n";
146
        $inMultilineComment = false;
147
        $importsAllowd = true;
148
        $wasCommentHack = false;
149
        while (!feof($source)) {
150
            if ($ext == 'css') {
151
                $line = fgets($source, 4096);
152
                $lineParse = trim($line);
153
                $lineParse_length = mb_strlen($lineParse, 'UTF-8');
154
                $newLine = '';
155
                // parse line char by char
156
                for ($i = 0; $i < $lineParse_length; $i++) {
157
                    $char = $lineParse[$i];
158
                    $nextchar = $i < ($lineParse_length - 1) ? $lineParse[$i + 1] : '';
159
                    if (!$inMultilineComment && $char == '/' && $nextchar == '*') {
160
                        // a multiline comment starts here
161
                        $inMultilineComment = true;
162
                        $wasCommentHack = false;
163
                        $newLine .= $char . $nextchar;
164
                        $i++;
165
                    } elseif ($inMultilineComment && $char == '*' && $nextchar == '/') {
166
                        // a multiline comment stops here
167
                        $inMultilineComment = false;
168
                        $newLine .= $char . $nextchar;
169
                        if (substr($lineParse, $i - 3, 8) == '/*\*//*/') {
170
                            $wasCommentHack = true;
171
                            $i += 3; // move to end of hack process hack as it where
172
                            $newLine .= '/*/'; // fix hack comment because we lost some chars with $i += 3
173
                        }
174
                        $i++;
175
                    } elseif ($importsAllowd && $char == '@' && substr($lineParse, $i, 7) == '@import') {
176
                        // an @import starts here
177
                        $lineParseRest = trim(substr($lineParse, $i + 7));
178
                        if (strtolower(substr($lineParseRest, 0, 3)) == 'url') {
179
                            // the @import uses url to specify the path
180
                            $posEnd = strpos($lineParse, ';', $i);
181
                            $charsEnd = substr($lineParse, $posEnd - 1, 2);
182
                            if ($charsEnd == ');') {
183
                                // used url() without media
184
                                $start = strpos($lineParseRest, '(') + 1;
185
                                $end = strpos($lineParseRest, ')');
186
                                $url = substr($lineParseRest, $start, $end - $start);
187 View Code Duplication
                                if ($url[0] == '"' | $url[0] == "'") {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: ($url[0] == '"') | $url[0] == '\'', Probably Intended Meaning: $url[0] == ('"' | $url[0] == '\'')

When comparing the result of a bit operation, we suggest to add explicit parenthesis and not to rely on PHP’s built-in operator precedence to ensure the code behaves as intended and to make it more readable.

Let’s take a look at these examples:

// Returns always int(0).
return 0 === $foo & 4;
return (0 === $foo) & 4;

// More likely intended return: true/false
return 0 === ($foo & 4);
Loading history...
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...
188
                                    $url = substr($url, 1, strlen($url) - 2);
189
                                }
190
                                // fix url
191
                                $url = dirname($file) . '/' . $url;
192 View Code Duplication
                                if (!$wasCommentHack) {
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...
193
                                    // clear buffer
194
                                    $contents[] = $newLine;
195
                                    $newLine = '';
196
                                    // process include
197
                                    $this->readFile($contents, $url, $ext);
198
                                } else {
199
                                    $newLine .= '@import url("' . $url . '");';
200
                                }
201
                                // skip @import statement
202
                                $i += $posEnd - $i;
203
                            } else {
204
                                // @import contains media type so we can't include its contents.
205
                                // We need to fix the url instead.
206
                                $start = strpos($lineParseRest, '(') + 1;
207
                                $end = strpos($lineParseRest, ')');
208
                                $url = substr($lineParseRest, $start, $end - $start);
209 View Code Duplication
                                if ($url[0] == '"' | $url[0] == "'") {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: ($url[0] == '"') | $url[0] == '\'', Probably Intended Meaning: $url[0] == ('"' | $url[0] == '\'')

When comparing the result of a bit operation, we suggest to add explicit parenthesis and not to rely on PHP’s built-in operator precedence to ensure the code behaves as intended and to make it more readable.

Let’s take a look at these examples:

// Returns always int(0).
return 0 === $foo & 4;
return (0 === $foo) & 4;

// More likely intended return: true/false
return 0 === ($foo & 4);
Loading history...
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...
210
                                    $url = substr($url, 1, strlen($url) - 2);
211
                                }
212
                                // fix url
213
                                $url = dirname($file) . '/' . $url;
214
                                // readd @import with fixed url
215
                                $newLine .= '@import url("' . $url . '")' . substr($lineParseRest, $end + 1, strpos($lineParseRest, ';') - $end - 1) . ';';
216
                                // skip @import statement
217
                                $i += $posEnd - $i;
218
                            }
219
                        } elseif (substr($lineParseRest, 0, 1) == '"' || substr($lineParseRest, 0, 1) == '\'') {
220
                            // the @import uses an normal string to specify the path
221
                            $posEnd = strpos($lineParseRest, ';');
222
                            $url = substr($lineParseRest, 1, $posEnd - 2);
223
                            $posEnd = strpos($lineParse, ';', $i);
224
                            // fix url
225
                            $url = dirname($file) . '/' . $url;
226 View Code Duplication
                            if (!$wasCommentHack) {
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...
227
                                // clear buffer
228
                                $contents[] = $newLine;
229
                                $newLine = '';
230
                                // process include
231
                                self::readFile($contents, $url, $ext);
232
                            } else {
233
                                $newLine .= '@import url("' . $url . '");';
234
                            }
235
                            // skip @import statement
236
                            $i += $posEnd - $i;
237
                        }
238
                    } elseif (!$inMultilineComment && $char != ' ' && $char != "\n" && $char != "\r\n" && $char != "\r") {
239
                        // css rule found -> stop processing of @import statements
240
                        $importsAllowd = false;
241
                        $newLine .= $char;
242
                    } else {
243
                        $newLine .= $char;
244
                    }
245
                }
246
                // fix other paths after @import processing
247
                if (!$importsAllowd) {
248
                    $relativePath = str_replace(realpath($this->rootDir), '', $file);
249
                    $newLine = self::cssFixPath($newLine, explode('/', dirname($relativePath)));
0 ignored issues
show
Documentation introduced by
explode('/', dirname($relativePath)) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
250
                }
251
                $contents[] = $newLine;
252
            } else {
253
                $contents[] = fgets($source, 4096);
254
            }
255
        }
256
        fclose($source);
257
        if ($ext == 'js') {
258
            $contents[] = "\n;\n";
259
        } else {
260
            $contents[] = "\n\n";
261
        }
262
    }
263
264
    /**
265
     * Fix paths in CSS files.
266
     * @param string $line CSS file line
267
     * @param string $relativeFilePath relative path to original file
268
     * @return string
269
     */
270
    private function cssFixPath($line, $relativeFilePath)
271
    {
272
        $regexpurl = '/url\([\'"]?([\.\/]*)(.*?)[\'"]?\)/i';
273
        if (false === strpos($line, 'url')) {
274
            return $line;
275
        }
276
277
        preg_match_all($regexpurl, $line, $matches, PREG_SET_ORDER);
278
        foreach ($matches as $match) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
279
            if ((strpos($match[1], '/') !== 0) && (substr($match[2], 0, 7) != 'http://') && (substr($match[2], 0, 8) != 'https://')) {
280
                $depth = substr_count($match[1], '../') * -1;
281
                $path = $depth < 0 ? array_slice($relativeFilePath, 0, $depth) : $relativeFilePath;
282
                $path = implode('/', $path);
283
                $path = !empty($path) ? $path . '/' : '/';
284
                $line = str_replace($match[0], "url('{$path}{$match[2]}')", $line);
285
            }
286
        }
287
288
        return $line;
289
    }
290
291
    /**
292
     * Remove comments, whitespace and spaces from css files
293
     * @param $contents
294
     * @return string
295
     */
296
    private function minify($contents)
297
    {
298
        // Remove comments.
299
        $contents = trim(preg_replace('/\/\*.*?\*\//s', '', $contents));
300
        // Compress whitespace.
301
        $contents = preg_replace('/\s+/', ' ', $contents);
302
        // Additional whitespace optimisation -- spaces around certain tokens is not required by CSS
303
        $contents = preg_replace('/\s*(;|\{|\}|:|,)\s*/', '\1', $contents);
304
305
        return $contents;
306
    }
307
}
308