Complex classes like Assets_processing 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 Assets_processing, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
17 | class Assets_processing { |
||
18 | /** |
||
19 | * Analyses file for images, fonts and css links and include they content into single resulting css file. |
||
20 | * |
||
21 | * Supports next file extensions for possible assets: |
||
22 | * jpeg, jpe, jpg, gif, png, ttf, ttc, svg, svgz, woff, css |
||
23 | * |
||
24 | * @param string $data Content of processed file |
||
25 | * @param string $file Path to file, that contains specified in previous parameter content |
||
26 | * @param string $target_directory_path Target directory for resulting combined files |
||
27 | * @param string[] $not_embedded_resources Some resources like images and fonts might not be embedded into resulting CSS because of their size |
||
28 | * |
||
29 | * @return string $data |
||
30 | */ |
||
31 | 10 | public static function css ($data, $file, $target_directory_path = PUBLIC_CACHE, &$not_embedded_resources = []) { |
|
32 | 10 | $dir = dirname($file); |
|
33 | /** |
||
34 | * Remove comments, tabs and new lines |
||
35 | */ |
||
36 | 10 | $data = preg_replace('#(/\*.*?\*/)|\t|\n|\r#s', ' ', $data); |
|
37 | /** |
||
38 | * Remove unnecessary spaces |
||
39 | */ |
||
40 | 10 | $data = preg_replace('/\s*([,;>{}(])\s*/', '$1', $data); |
|
41 | 10 | $data = preg_replace('/\s+/', ' ', $data); |
|
42 | /** |
||
43 | * Return spaces required in media queries |
||
44 | */ |
||
45 | 10 | $data = preg_replace('/\s(and|or)\(/', ' $1 (', $data); |
|
46 | /** |
||
47 | * Duplicated semicolons |
||
48 | */ |
||
49 | 10 | $data = preg_replace('/;+/m', ';', $data); |
|
50 | /** |
||
51 | * Minify rgb colors declarations |
||
52 | */ |
||
53 | 10 | $data = preg_replace_callback( |
|
54 | 10 | '/rgb\(([0-9,.]+)\)/i', |
|
55 | function ($rgb) { |
||
56 | 6 | $rgb = explode(',', $rgb[1]); |
|
57 | return |
||
58 | '#'. |
||
59 | 6 | str_pad(dechex($rgb[0]), 2, 0, STR_PAD_LEFT). |
|
60 | 6 | str_pad(dechex($rgb[1]), 2, 0, STR_PAD_LEFT). |
|
61 | 6 | str_pad(dechex($rgb[2]), 2, 0, STR_PAD_LEFT); |
|
62 | 10 | }, |
|
63 | 10 | $data |
|
64 | ); |
||
65 | /** |
||
66 | * Minify repeated colors declarations |
||
67 | */ |
||
68 | 10 | $data = preg_replace('/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/i', '#$1$2$3', $data); |
|
69 | /** |
||
70 | * Remove unnecessary zeros |
||
71 | */ |
||
72 | 10 | $data = preg_replace('/(\D)0\.(\d+)/i', '$1.$2', $data); |
|
73 | /** |
||
74 | * Unnecessary spaces around colons (should have whitespace character after, otherwise `.class :disabled` will be handled incorrectly) |
||
75 | */ |
||
76 | 10 | $data = preg_replace('/\s*:\s+/', ':', $data); |
|
77 | /** |
||
78 | * Assets processing |
||
79 | */ |
||
80 | // TODO: replace by loop, track duplicated stuff that are subject to inlining and if they appear more than once, don't inline them |
||
81 | 10 | $data = preg_replace_callback( |
|
82 | 10 | '/url\((.*)\)|@import\s*(?:url\()?\s*([\'"].*[\'"])\s*\)??.*;/U', |
|
83 | function ($match) use ($dir, $target_directory_path, &$not_embedded_resources) { |
||
84 | 10 | $path_matched = $match[2] ?? $match[1]; |
|
85 | 10 | $path = trim($path_matched, '\'" '); |
|
86 | 10 | $link = explode('?', $path, 2)[0]; |
|
87 | 10 | if (!static::is_relative_path_and_exists($link, $dir)) { |
|
88 | 4 | return $match[0]; |
|
89 | } |
||
90 | 10 | $extension = file_extension($link); |
|
91 | 10 | $absolute_path = static::absolute_path($link, $dir); |
|
92 | 10 | $content = file_get_contents($absolute_path); |
|
93 | /** |
||
94 | * Process recursively CSS imports, but ignore non embedded resources there |
||
95 | */ |
||
96 | /** @noinspection UsageOfSilenceOperatorInspection */ |
||
97 | 10 | if ($extension == 'css' && @$match[2]) { |
|
98 | 6 | $content = static::css($content, $absolute_path, $target_directory_path); |
|
99 | } |
||
100 | 10 | $filename = static::file_put_contents_with_hash($target_directory_path, $extension, $content); |
|
101 | /** @noinspection UsageOfSilenceOperatorInspection */ |
||
102 | /** |
||
103 | * We ignore files with query parameters as well as CSS imports with media queries |
||
104 | */ |
||
105 | 10 | if (strpos($path, '?') === false && !trim(@$match[3])) { |
|
106 | 6 | $not_embedded_resources[] = str_replace(getcwd(), '', "$target_directory_path/$filename"); |
|
107 | } |
||
108 | 10 | return str_replace($path_matched, "'./$filename'", $match[0]); |
|
109 | 10 | }, |
|
110 | 10 | $data |
|
111 | ); |
||
112 | 10 | return trim($data); |
|
113 | } |
||
114 | /** |
||
115 | * Put `$content` into `$dir` where filename is `md5($content)` with specified extension |
||
116 | * |
||
117 | * @param string $dir |
||
118 | * @param string $extension |
||
119 | * @param string $content |
||
120 | * |
||
121 | * @return string Filename (without full path) |
||
122 | */ |
||
123 | 10 | protected static function file_put_contents_with_hash ($dir, $extension, $content) { |
|
124 | 10 | $hash = md5($content); |
|
125 | 10 | file_put_contents("$dir/$hash.$extension", $content, LOCK_EX | FILE_BINARY); |
|
126 | 10 | return "$hash.$extension"; |
|
127 | } |
||
128 | /** |
||
129 | * Simple and fast JS minification |
||
130 | * |
||
131 | * @param string $data |
||
132 | * |
||
133 | * @return string |
||
134 | */ |
||
135 | 10 | public static function js ($data) { |
|
136 | /** |
||
137 | * Split into array of lines |
||
138 | */ |
||
139 | 10 | $data = explode("\n", $data); |
|
140 | /** |
||
141 | * Flag that is `true` when inside comment |
||
142 | */ |
||
143 | 10 | $in_comment = false; |
|
144 | 10 | $continue_after_position = -1; |
|
145 | 10 | foreach ($data as $index => &$current_line) { |
|
146 | 10 | if ($continue_after_position >= $index) { |
|
147 | 2 | continue; |
|
148 | } |
||
149 | 10 | $next_line = isset($data[$index + 1]) ? trim($data[$index + 1]) : ''; |
|
150 | /** |
||
151 | * Remove starting and trailing spaces |
||
152 | */ |
||
153 | 10 | $current_line = trim($current_line); |
|
154 | /** |
||
155 | * Remove single-line comments |
||
156 | */ |
||
157 | 10 | if (mb_strpos($current_line, '//') === 0) { |
|
158 | 10 | $current_line = ''; |
|
159 | 10 | continue; |
|
160 | } |
||
161 | /** |
||
162 | * Starts with multi-line comment |
||
163 | */ |
||
164 | 10 | if (mb_strpos($current_line, '/*') === 0) { |
|
165 | 10 | $in_comment = true; |
|
166 | } |
||
167 | 10 | if (!$in_comment) { |
|
168 | 10 | $backticks_position = strpos($current_line, '`'); |
|
169 | /** |
||
170 | * Handling template strings can be tricky (since they might be multi-line), so let's fast-forward to the last backticks position and continue |
||
171 | * from there |
||
172 | */ |
||
173 | 10 | if ($backticks_position !== false) { |
|
174 | 6 | $last_item_with_backticks = array_keys( |
|
175 | 6 | array_filter( |
|
176 | 6 | $data, |
|
177 | function ($d) { |
||
178 | 6 | return strpos($d, '`') !== false; |
|
179 | 6 | } |
|
180 | ) |
||
181 | ); |
||
182 | 6 | $last_item_with_backticks = array_pop($last_item_with_backticks); |
|
183 | 6 | if ($last_item_with_backticks > $index) { |
|
184 | 2 | $continue_after_position = $last_item_with_backticks; |
|
185 | 2 | continue; |
|
186 | } |
||
187 | } |
||
188 | /** |
||
189 | * Add new line at the end if only needed |
||
190 | */ |
||
191 | 10 | if (static::new_line_needed($current_line, $next_line)) { |
|
192 | 10 | $current_line .= "\n"; |
|
193 | } |
||
194 | /** |
||
195 | * Single-line comment |
||
196 | */ |
||
197 | 10 | $current_line = preg_replace('#^\s*//[^\'"]+$#', '', $current_line); |
|
198 | /** |
||
199 | * If we are not sure - just add new line afterwards |
||
200 | */ |
||
201 | 10 | $current_line = preg_replace('#//.*$#', "\\0\n", $current_line); |
|
202 | } else { |
||
203 | /** |
||
204 | * End of multi-line comment |
||
205 | */ |
||
206 | 10 | if (strpos($current_line, '*/') !== false) { |
|
207 | 10 | $current_line = explode('*/', $current_line)[1]; |
|
208 | 10 | $in_comment = false; |
|
209 | } else { |
||
210 | 10 | $current_line = ''; |
|
211 | } |
||
212 | } |
||
213 | } |
||
214 | 10 | $data = implode('', $data); |
|
215 | 10 | $data = str_replace('</script>', '<\/script>', $data); |
|
216 | 10 | return trim($data, ';').';'; |
|
217 | } |
||
218 | /** |
||
219 | * @param string $current_line |
||
220 | * @param string $next_line |
||
221 | * |
||
222 | * @return bool |
||
223 | */ |
||
224 | 10 | protected static function new_line_needed ($current_line, $next_line) { |
|
225 | /** |
||
226 | * Set of symbols that are safe to be concatenated without new line with anything else |
||
227 | */ |
||
228 | $regexp = /** @lang PhpRegExp */ |
||
229 | 10 | '[:;,.+\-*/{}?><^\'"\[\]=&(]'; |
|
230 | return |
||
231 | 10 | $current_line && |
|
232 | 10 | $next_line && |
|
233 | 10 | !preg_match("#$regexp\$#", $current_line) && |
|
234 | 10 | !preg_match("#^$regexp#", $next_line); |
|
235 | } |
||
236 | /** |
||
237 | * Analyses file for scripts and styles, combines them into resulting files in order to optimize loading process |
||
238 | * (files with combined scripts and styles will be created) |
||
239 | * |
||
240 | * @param string $data Content of processed file |
||
241 | * @param string $file Path to file, that contains specified in previous parameter content |
||
242 | * @param string $target_directory_path Target directory for resulting combined files |
||
243 | * @param bool $vulcanization Whether to put combined files separately or to make included assets built-in (vulcanization) |
||
244 | * @param string[] $not_embedded_resources Resources like images/fonts might not be embedded into resulting CSS because of big size or CSS/JS because of CSP |
||
245 | * |
||
246 | * @return string |
||
247 | */ |
||
248 | 8 | public static function html ($data, $file, $target_directory_path, $vulcanization, &$not_embedded_resources = []) { |
|
249 | 8 | static::html_process_links_and_styles($data, $file, $target_directory_path, $vulcanization, $not_embedded_resources); |
|
250 | 8 | static::html_process_scripts($data, $file, $target_directory_path, $vulcanization, $not_embedded_resources); |
|
251 | // Removing HTML comments (those that are mostly likely comments, to avoid problems) |
||
252 | 8 | $data = preg_replace_callback( |
|
253 | 8 | '/^\s*<!--([^>-].*[^-])?-->/Ums', |
|
254 | function ($matches) { |
||
255 | 8 | return mb_strpos('--', $matches[1]) === false ? '' : $matches[0]; |
|
256 | 8 | }, |
|
257 | 8 | $data |
|
258 | ); |
||
259 | 8 | return preg_replace("/\n+/", "\n", $data); |
|
260 | } |
||
261 | /** |
||
262 | * @param string $data Content of processed file |
||
263 | * @param string $file Path to file, that contains specified in previous parameter content |
||
264 | * @param string $target_directory_path Target directory for resulting combined files |
||
265 | * @param bool $vulcanization Whether to put combined files separately or to make included assets built-in (vulcanization) |
||
266 | * @param string[] $not_embedded_resources Resources like images/fonts might not be embedded into resulting CSS because of big size or CSS/JS because of CSP |
||
267 | */ |
||
268 | 8 | protected static function html_process_scripts (&$data, $file, $target_directory_path, $vulcanization, &$not_embedded_resources) { |
|
269 | 8 | if (!preg_match_all('/<script(.*)<\/script>/Uims', $data, $scripts)) { |
|
270 | 8 | return; |
|
271 | } |
||
272 | 8 | $scripts_content = ''; |
|
273 | 8 | $scripts_to_replace = []; |
|
274 | 8 | $dir = dirname($file); |
|
275 | 8 | foreach ($scripts[1] as $index => $script) { |
|
276 | 8 | $script = explode('>', $script, 2); |
|
277 | 8 | if (preg_match('/src\s*=\s*[\'"](.*)[\'"]/Uims', $script[0], $url)) { |
|
278 | 8 | $url = $url[1]; |
|
279 | 8 | if (!static::is_relative_path_and_exists($url, $dir)) { |
|
280 | 4 | continue; |
|
281 | } |
||
282 | 8 | $scripts_to_replace[] = $scripts[0][$index]; |
|
283 | 8 | $scripts_content .= file_get_contents("$dir/$url").";\n"; |
|
284 | } else { |
||
285 | 8 | $scripts_to_replace[] = $scripts[0][$index]; |
|
286 | 8 | $scripts_content .= "$script[1];\n"; |
|
287 | } |
||
288 | } |
||
289 | 8 | $scripts_content = static::js($scripts_content); |
|
290 | 8 | if (!$scripts_to_replace) { |
|
291 | 4 | return; |
|
292 | } |
||
293 | // Remove all scripts |
||
294 | 8 | $data = str_replace($scripts_to_replace, '', $data); |
|
295 | /** |
||
296 | * If vulcanization is not used - put contents into separate file, and put link to it, otherwise put minified content back |
||
297 | */ |
||
298 | 8 | if (!$vulcanization) { |
|
299 | 2 | $filename = static::file_put_contents_with_hash($target_directory_path, 'js', $scripts_content); |
|
300 | // Add script with combined content file to the end |
||
301 | 2 | $data .= "<script src=\"./$filename\"></script>"; |
|
302 | 2 | $not_embedded_resources[] = str_replace(getcwd(), '', "$target_directory_path/$filename"); |
|
303 | } else { |
||
304 | // Add combined content inline script to the end |
||
305 | 6 | $data .= "<script>$scripts_content</script>"; |
|
306 | } |
||
307 | 8 | } |
|
308 | /** |
||
309 | * @param string $data Content of processed file |
||
310 | * @param string $file Path to file, that contains specified in previous parameter content |
||
311 | * @param string $target_directory_path Target directory for resulting combined files |
||
312 | * @param bool $vulcanization Whether to put combined files separately or to make included assets built-in (vulcanization) |
||
313 | * @param string[] $not_embedded_resources Resources like images/fonts might not be embedded into resulting CSS because of big size or CSS/JS because of CSP |
||
314 | */ |
||
315 | 8 | protected static function html_process_links_and_styles (&$data, $file, $target_directory_path, $vulcanization, &$not_embedded_resources) { |
|
316 | 8 | if (!preg_match_all('/<link(.*)>|<style(.*)<\/style>/Uims', $data, $links_and_styles)) { |
|
317 | 8 | return; |
|
318 | } |
||
319 | 8 | $dir = dirname($file); |
|
320 | // With reverse order we can add inlined CSS imports directly to the beginning of `<template>` element |
||
321 | 8 | $links_and_styles = array_map( |
|
322 | 8 | function ($item) { |
|
323 | 8 | return array_reverse($item, true); |
|
324 | 8 | }, |
|
325 | 8 | $links_and_styles |
|
326 | ); |
||
327 | 8 | foreach ($links_and_styles[1] as $index => $link) { |
|
328 | /** |
||
329 | * For plain styles we do not do anything fancy besides minifying its sources (no rearrangement or anything like that) |
||
330 | */ |
||
331 | 8 | if (mb_strpos($links_and_styles[0][$index], '</style>') > 0) { |
|
332 | 8 | $content = explode('>', $links_and_styles[2][$index], 2)[1]; |
|
333 | 8 | $data = str_replace( |
|
334 | 8 | $content, |
|
335 | 8 | static::css($content, $file, $target_directory_path, $not_embedded_resources), |
|
336 | 8 | $data |
|
337 | ); |
||
338 | 8 | continue; |
|
339 | } |
||
340 | 8 | if (!static::has_relative_href($link, $url, $dir)) { |
|
341 | 4 | continue; |
|
342 | } |
||
343 | 8 | $import = preg_match('/rel\s*=\s*[\'"]import[\'"]/Uim', $link); |
|
344 | /** |
||
345 | * CSS imports are available in Polymer alongside with HTML imports |
||
346 | */ |
||
347 | 8 | $css_import = $import && preg_match('/type\s*=\s*[\'"]css[\'"]/Uim', $link); |
|
348 | 8 | $stylesheet = preg_match('/rel\s*=\s*[\'"]stylesheet[\'"]/Uim', $link); |
|
349 | /** |
||
350 | * TODO: Polymer only supports `custom-style > style`, but no `link`-based counterpart, so we can't provide CSP-compatibility in general, |
||
351 | * thus always inlining styles into HTML |
||
352 | */ |
||
353 | 8 | if ($css_import || $stylesheet) { |
|
354 | /** |
||
355 | * If content is link to CSS file |
||
356 | */ |
||
357 | 8 | $css = static::css( |
|
358 | 8 | file_get_contents("$dir/$url"), |
|
359 | 8 | "$dir/$url", |
|
360 | 8 | $target_directory_path, |
|
361 | 8 | $not_embedded_resources |
|
362 | ); |
||
363 | 8 | $data = preg_replace( |
|
364 | 8 | '/'.preg_quote($links_and_styles[0][$index], '/').'(.*)<template>/Uims', |
|
365 | 8 | "$1<template><style>$css</style>", |
|
366 | 8 | $data |
|
367 | ); |
||
368 | 8 | $data = preg_replace( |
|
369 | 8 | '/(<template><style>[^<]+)<\/style><style>/Uim', |
|
370 | 8 | '$1', |
|
371 | 8 | $data |
|
372 | ); |
||
373 | 4 | } elseif ($import) { |
|
374 | /** |
||
375 | * If content is HTML import |
||
376 | */ |
||
377 | 4 | $data = str_replace( |
|
378 | 4 | $links_and_styles[0][$index], |
|
379 | 4 | static::html( |
|
380 | 4 | file_get_contents("$dir/$url"), |
|
381 | 4 | "$dir/$url", |
|
382 | 4 | $target_directory_path, |
|
383 | 4 | $vulcanization, |
|
384 | 4 | $not_embedded_resources |
|
385 | ), |
||
386 | 8 | $data |
|
387 | ); |
||
388 | } |
||
389 | } |
||
390 | 8 | } |
|
391 | /** |
||
392 | * @param string $link |
||
393 | * @param string $url |
||
394 | * @param string $dir |
||
395 | * |
||
396 | * @return bool |
||
397 | */ |
||
398 | 8 | protected static function has_relative_href ($link, &$url, $dir) { |
|
399 | $result = |
||
400 | 8 | $link && |
|
401 | 8 | preg_match('/href\s*=\s*[\'"](.*)[\'"]/Uims', $link, $url); |
|
402 | 8 | if ($result && static::is_relative_path_and_exists($url[1], $dir)) { |
|
403 | 8 | $url = $url[1]; |
|
404 | 8 | return true; |
|
405 | } |
||
406 | 4 | return false; |
|
407 | } |
||
408 | /** |
||
409 | * @param string $path |
||
410 | * @param string $dir |
||
411 | * |
||
412 | * @return bool |
||
413 | */ |
||
414 | 10 | protected static function is_relative_path_and_exists ($path, $dir) { |
|
417 | /** |
||
418 | * @param string $path |
||
419 | * @param string $dir |
||
420 | * |
||
421 | * @return string |
||
422 | */ |
||
423 | 10 | protected static function absolute_path ($path, $dir) { |
|
429 | } |
||
430 |