Completed
Push — master ( 2dbb1e...a190f2 )
by Nazar
03:57
created

Includes   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 503
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 99.51%

Importance

Changes 0
Metric Value
dl 0
loc 503
ccs 204
cts 205
cp 0.9951
rs 5.7894
c 0
b 0
f 0
wmc 65
lcom 1
cbo 8

25 Methods

Rating   Name   Duplication   Size   Complexity  
A init_includes() 0 11 1
A core_html() 0 3 1
A core_js() 0 3 1
A core_css() 0 3 1
A config() 0 3 1
A config_internal() 0 16 2
B add_includes_on_page() 0 51 4
A page_compression_usage() 0 3 3
A add_script_imports_to_document() 0 7 2
A get_includes_and_preload_resource_for_page_with_compression() 0 12 3
B get_dependency_component() 0 12 7
A get_includes_for_page_without_compression() 0 6 1
A absolute_path_to_relative() 0 3 1
A add_versions_hash() 0 16 3
A add_includes_on_page_manually_added() 0 14 3
A add_includes_on_page_manually_added_normal() 0 18 1
A html() 0 3 1
A js() 0 3 1
A css() 0 3 1
B include_common() 0 17 5
A edge() 0 8 2
B webcomponents_polyfill() 0 11 5
C get_normalized_includes() 0 40 7
A add_preload() 0 13 3
B add_includes_on_page_manually_added_frontend_load_optimization() 0 30 5

How to fix   Complexity   

Complex Class

Complex classes like Includes 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 Includes, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2014-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Page;
9
use
10
	cs\App,
11
	cs\Config,
12
	cs\Language,
13
	cs\Request,
14
	cs\Response,
15
	h,
16
	cs\Page\Includes\Cache,
17
	cs\Page\Includes\Collecting,
18
	cs\Page\Includes\RequireJS;
19
20
/**
21
 * Includes management for `cs\Page` class
22
 *
23
 * @property string $Title
24
 * @property string $Description
25
 * @property string $canonical_url
26
 * @property string $Head
27
 * @property string $post_Body
28
 * @property string $theme
29
 */
30
trait Includes {
31
	use
32
		Cache,
33
		Collecting,
34
		RequireJS;
35
	protected $extension_to_as = [
36
		'jpeg' => 'image',
37
		'jpe'  => 'image',
38
		'jpg'  => 'image',
39
		'gif'  => 'image',
40
		'png'  => 'image',
41
		'svg'  => 'image',
42
		'svgz' => 'image',
43
		'woff' => 'font',
44
		//'woff2' => 'font',
45
		'css'  => 'style',
46
		'js'   => 'script',
47
		'html' => 'document'
48
	];
49
	/**
50
	 * @var array
51
	 */
52
	protected $core_html;
53
	/**
54
	 * @var array
55
	 */
56
	protected $core_js;
57
	/**
58
	 * @var array
59
	 */
60
	protected $core_css;
61
	/**
62
	 * @var string
63
	 */
64
	protected $core_config;
65
	/**
66
	 * @var array
67
	 */
68
	protected $html;
69
	/**
70
	 * @var array
71
	 */
72
	protected $js;
73
	/**
74
	 * @var array
75
	 */
76
	protected $css;
77
	/**
78
	 * @var string
79
	 */
80
	protected $config;
81
	/**
82
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
83
	 * @var string
84
	 */
85
	protected $pcache_basename_path;
86 34
	protected function init_includes () {
87 34
		$this->core_html            = [];
88 34
		$this->core_js              = [];
89 34
		$this->core_css             = [];
90 34
		$this->core_config          = '';
91 34
		$this->html                 = [];
92 34
		$this->js                   = [];
93 34
		$this->css                  = [];
94 34
		$this->config               = '';
95 34
		$this->pcache_basename_path = '';
96 34
	}
97
	/**
98
	 * @param string|string[] $add
99
	 *
100
	 * @return \cs\Page
101
	 */
102 4
	protected function core_html ($add) {
103 4
		return $this->include_common('html', $add, true);
104
	}
105
	/**
106
	 * @param string|string[] $add
107
	 *
108
	 * @return \cs\Page
109
	 */
110 4
	protected function core_js ($add) {
111 4
		return $this->include_common('js', $add, true);
112
	}
113
	/**
114
	 * @param string|string[] $add
115
	 *
116
	 * @return \cs\Page
117
	 */
118 4
	protected function core_css ($add) {
119 4
		return $this->include_common('css', $add, true);
120
	}
121
	/**
122
	 * Including of Web Components
123
	 *
124
	 * @param string|string[] $add Path to including file, or code
125
	 *
126
	 * @return \cs\Page
127
	 */
128 2
	public function html ($add) {
129 2
		return $this->include_common('html', $add, false);
130
	}
131
	/**
132
	 * Including of JavaScript
133
	 *
134
	 * @param string|string[] $add Path to including file, or code
135
	 *
136
	 * @return \cs\Page
137
	 */
138 2
	public function js ($add) {
139 2
		return $this->include_common('js', $add, false);
140
	}
141
	/**
142
	 * Including of CSS
143
	 *
144
	 * @param string|string[] $add Path to including file, or code
145
	 *
146
	 * @return \cs\Page
147
	 */
148 2
	public function css ($add) {
149 2
		return $this->include_common('css', $add, false);
150
	}
151
	/**
152
	 * @param string          $what
153
	 * @param string|string[] $add
154
	 * @param bool            $core
155
	 *
156
	 * @return \cs\Page
157
	 */
158 4
	protected function include_common ($what, $add, $core) {
159 4
		if (!$add) {
160 2
			return $this;
161
		}
162 4
		if (is_array($add)) {
163 4
			foreach (array_filter($add) as $a) {
164 4
				$this->include_common($what, $a, $core);
165
			}
166
		} else {
167 4
			if ($core) {
168 4
				$what = "core_$what";
169
			}
170 4
			$target   = &$this->$what;
171 4
			$target[] = $add;
172
		}
173 4
		return $this;
174
	}
175
	/**
176
	 * Add config on page to make it available on frontend
177
	 *
178
	 * @param mixed  $config_structure        Any scalar type or array
179
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
180
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
181
	 *
182
	 * @return \cs\Page
183
	 */
184 4
	public function config ($config_structure, $target) {
185 4
		return $this->config_internal($config_structure, $target);
186
	}
187
	/**
188
	 * @param mixed  $config_structure
189
	 * @param string $target
190
	 * @param bool   $core
191
	 *
192
	 * @return \cs\Page
193
	 */
194 4
	protected function config_internal ($config_structure, $target, $core = false) {
195 4
		$config = h::script(
196 4
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
197
			[
198 4
				'target' => $target,
199 4
				'class'  => 'cs-config',
200 4
				'type'   => 'application/json'
201
			]
202
		);
203 4
		if ($core) {
204 4
			$this->core_config .= $config;
205
		} else {
206 4
			$this->config .= $config;
207
		}
208 4
		return $this;
209
	}
210
	/**
211
	 * Getting of HTML, JS and CSS includes
212
	 *
213
	 * @return \cs\Page
214
	 */
215 4
	protected function add_includes_on_page () {
216 4
		$Config = Config::instance(true);
217 4
		if (!$Config) {
218
			return $this;
219
		}
220
		/**
221
		 * Base name for cache files
222
		 */
223 4
		$this->pcache_basename_path = PUBLIC_CACHE.'/'.$this->theme;
224
		// TODO: I hope some day we'll get rid of this sh*t :(
225 4
		$this->edge();
226 4
		$Request = Request::instance();
227
		/**
228
		 * If CSS and JavaScript compression enabled
229
		 */
230 4
		$L = Language::instance();
231 4
		if ($this->page_compression_usage($Config, $Request)) {
232
			/**
233
			 * Rebuilding HTML, JS and CSS cache if necessary
234
			 */
235 4
			$this->rebuild_cache($Config, $L);
236 4
			$this->webcomponents_polyfill($Request, $Config, true);
237 4
			$languages_hash = $this->get_hash_of(implode('', $Config->core['active_languages']));
238 4
			$language_hash  = file_get_json(PUBLIC_CACHE."/languages-$languages_hash.json")[$L->clanguage];
239 4
			$this->config_internal(
240
				[
241 4
					'language' => $L->clanguage,
242 4
					'hash'     => $language_hash
243
				],
244 4
				'cs.current_language',
245 4
				true
246
			);
247 4
			list($includes, $preload) = $this->get_includes_and_preload_resource_for_page_with_compression($Request);
248
		} else {
249 2
			$this->webcomponents_polyfill($Request, $Config, false);
250
			/**
251
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
252
			 */
253 2
			$this->config_internal($L, 'cs.Language', true);
254 2
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
255 2
			$includes = $this->get_includes_for_page_without_compression($Config, $Request);
256 2
			$preload  = [];
257
		}
258 4
		$this->core_css($includes['css']);
259 4
		$this->core_js($includes['js']);
260 4
		if (isset($includes['html'])) {
261 4
			$this->core_html($includes['html']);
262
		}
263 4
		$this->add_includes_on_page_manually_added($Config, $Request, $preload);
264 4
		return $this;
265
	}
266
	/**
267
	 * @param Config  $Config
268
	 * @param Request $Request
269
	 *
270
	 * @return bool
271
	 */
272 4
	protected function page_compression_usage ($Config, $Request) {
273 4
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
274
	}
275
	/**
276
	 * Add JS polyfills for IE/Edge
277
	 */
278 4
	protected function edge () {
279 4
		if (strpos(Request::instance()->header('user-agent'), 'Edge') === false) {
280 4
			return;
281
		}
282 2
		$this->core_js(
283 2
			get_files_list(DIR.'/includes/js/microsoft_sh*t', '/.*\.js$/i', 'f', 'includes/js/microsoft_sh*t', true)
284
		);
285 2
	}
286
	/**
287
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
288
	 *
289
	 * @param Request $Request
290
	 * @param Config  $Config
291
	 * @param bool    $with_compression
292
	 */
293 4
	protected function webcomponents_polyfill ($Request, $Config, $with_compression) {
294 4
		if (($this->theme != Config::SYSTEM_THEME && $Config->core['disable_webcomponents']) || $Request->cookie('shadow_dom') == 1) {
295 2
			return;
296
		}
297 4
		if ($with_compression) {
298 4
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
299 4
			$this->add_script_imports_to_document($Config, "<script src=\"/storage/pcache/webcomponents.js?$hash\"></script>\n");
300
		} else {
301 2
			$this->add_script_imports_to_document($Config, "<script src=\"/includes/js/WebComponents-polyfill/webcomponents-custom.min.js\"></script>\n");
302
		}
303 4
	}
304
	/**
305
	 * @param Config $Config
306
	 * @param string $content
307
	 */
308 4
	protected function add_script_imports_to_document ($Config, $content) {
309 4
		if ($Config->core['put_js_after_body']) {
310 4
			$this->post_Body .= $content;
311
		} else {
312 2
			$this->Head .= $content;
313
		}
314 4
	}
315
	/**
316
	 * @param Request $Request
317
	 *
318
	 * @return array[]
319
	 */
320 4
	protected function get_includes_and_preload_resource_for_page_with_compression ($Request) {
321 4
		list($dependencies, $compressed_includes_map, $not_embedded_resources_map) = file_get_json("$this->pcache_basename_path.json");
322 4
		$includes = $this->get_normalized_includes($dependencies, $compressed_includes_map, $Request);
323 4
		$preload  = [];
324 4
		foreach (array_merge(...array_values($includes)) as $path) {
325 4
			$preload[] = [$path];
326 4
			if (isset($not_embedded_resources_map[$path])) {
327 4
				$preload[] = $not_embedded_resources_map[$path];
328
			}
329
		}
330 4
		return [$includes, array_merge(...$preload)];
331
	}
332
	/**
333
	 * @param array      $dependencies
334
	 * @param string[][] $includes_map
335
	 * @param Request    $Request
336
	 *
337
	 * @return string[][]
338
	 */
339 4
	protected function get_normalized_includes ($dependencies, $includes_map, $Request) {
340 4
		$current_module = $Request->current_module;
341
		/**
342
		 * Current URL based on controller path (it better represents how page was rendered)
343
		 */
344 4
		$current_url = array_slice(App::instance()->controller_path, 1);
345 4
		$current_url = ($Request->admin_path ? 'admin/' : '')."$current_module/".implode('/', $current_url);
346
		/**
347
		 * Narrow the dependencies to current module only
348
		 */
349 4
		$dependencies    = array_unique(
350
			array_merge(
351 4
				['System'],
352 4
				$dependencies['System'],
353 4
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
354
			)
355
		);
356 4
		$system_includes = [];
357
		// Array with empty array in order to avoid `array_merge()` failure later
358 4
		$dependencies_includes = array_fill_keys($dependencies, [[]]);
359 4
		$includes              = [];
360 4
		foreach ($includes_map as $path => $local_includes) {
361 4
			if ($path == 'System') {
362 4
				$system_includes = $local_includes;
363 4
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
364
				/**
365
				 * @var string $component
366
				 */
367 2
				$dependencies_includes[$component][] = $local_includes;
368 4
			} elseif (mb_strpos($current_url, $path) === 0) {
369 4
				$includes[] = $local_includes;
370
			}
371
		}
372
		// Convert to indexed array first
373 4
		$dependencies_includes = array_values($dependencies_includes);
374
		// Flatten array on higher level
375 4
		$dependencies_includes = array_merge(...$dependencies_includes);
376
		// Hack: 2 array_merge_recursive() just to be compatible with HHVM, simplify when https://github.com/facebook/hhvm/issues/7087 is resolved
377 4
		return _array(array_merge_recursive(array_merge_recursive($system_includes, ...$dependencies_includes), ...$includes));
378
	}
379
	/**
380
	 * @param array   $dependencies
381
	 * @param string  $url
382
	 * @param Request $Request
383
	 *
384
	 * @return false|string
385
	 */
386 4
	protected function get_dependency_component ($dependencies, $url, $Request) {
387 4
		$url_exploded = explode('/', $url);
388
		/** @noinspection NestedTernaryOperatorInspection */
389 4
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
390
		$is_dependency =
391 4
			$url_component !== Config::SYSTEM_MODULE &&
392 4
			in_array($url_component, $dependencies) &&
393
			(
394 4
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
395
			);
396 4
		return $is_dependency ? $url_component : false;
397
	}
398
	/**
399
	 * @param Config  $Config
400
	 * @param Request $Request
401
	 *
402
	 * @return string[][]
403
	 */
404 2
	protected function get_includes_for_page_without_compression ($Config, $Request) {
405
		// To determine all dependencies and stuff we need `$Config` object to be already created
406 2
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
407 2
		$includes = $this->get_normalized_includes($dependencies, $includes_map, $Request);
408 2
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
409
	}
410
	/**
411
	 * @param string[]|string[][] $path
412
	 *
413
	 * @return string[]|string[][]
414
	 */
415 6
	protected function absolute_path_to_relative ($path) {
416 6
		return _substr($path, strlen(DIR));
417
	}
418
	/**
419
	 * @param string[][] $includes
420
	 *
421
	 * @return string[][]
422
	 */
423 2
	protected function add_versions_hash ($includes) {
424 2
		$content      = array_reduce(
425 2
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
426
			function ($content, $file) {
427
				return $content.file_get_contents($file);
428 2
			}
429
		);
430 2
		$content_hash = $this->get_hash_of($content);
431 2
		foreach ($includes as &$files) {
432 2
			foreach ($files as &$file) {
433 2
				$file .= "?$content_hash";
434
			}
435 2
			unset($file);
436
		}
437 2
		return $includes;
438
	}
439
	/**
440
	 * @param Config   $Config
441
	 * @param Request  $Request
442
	 * @param string[] $preload
443
	 */
444 4
	protected function add_includes_on_page_manually_added ($Config, $Request, $preload) {
445
		/** @noinspection NestedTernaryOperatorInspection */
446 4
		$this->Head .= array_reduce(
447 4
			array_merge($this->core_css, $this->css),
448
			function ($content, $href) {
449 4
				return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
450 4
			}
451
		);
452 4
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
453 4
			$this->add_includes_on_page_manually_added_frontend_load_optimization($Config, $Request);
454
		} else {
455 2
			$this->add_includes_on_page_manually_added_normal($Config, $Request, $preload);
456
		}
457 4
	}
458
	/**
459
	 * @param Config   $Config
460
	 * @param Request  $Request
461
	 * @param string[] $preload
462
	 */
463 2
	protected function add_includes_on_page_manually_added_normal ($Config, $Request, $preload) {
464 2
		$this->add_preload($preload, $Request);
465 2
		$configs      = $this->core_config.$this->config;
466 2
		$scripts      = array_reduce(
467 2
			array_merge($this->core_js, $this->js),
468
			function ($content, $src) {
469 2
				return "$content<script src=\"$src\"></script>\n";
470 2
			}
471
		);
472 2
		$html_imports = array_reduce(
473 2
			array_merge($this->core_html, $this->html),
474 2
			function ($content, $href) {
475 2
				return "$content<link href=\"$href\" rel=\"import\">\n";
476 2
			}
477
		);
478 2
		$this->Head .= $configs;
479 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
480 2
	}
481
	/**
482
	 * @param string[] $preload
483
	 * @param Request  $Request
484
	 */
485 4
	protected function add_preload ($preload, $Request) {
486 4
		if ($Request->cookie('pushed')) {
487 2
			return;
488
		}
489 4
		$Response = Response::instance();
490 4
		$Response->cookie('pushed', 1, 0, true);
491 4
		foreach ($preload as $resource) {
492 4
			$extension = explode('?', file_extension($resource))[0];
493 4
			$as        = $this->extension_to_as[$extension];
494 4
			$resource  = str_replace(' ', '%20', $resource);
495 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
496
		}
497 4
	}
498
	/**
499
	 * @param Config  $Config
500
	 * @param Request $Request
501
	 */
502 4
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config, $Request) {
503 4
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
504 4
		$this->add_preload(
505
			array_unique(
506 4
				array_merge($preload, $this->core_css, $this->css)
507
			),
508
			$Request
509
		);
510 4
		$system_scripts    = '';
511 4
		$optimized_scripts = [];
512 4
		$system_imports    = '';
513 4
		$optimized_imports = [];
514 4
		foreach (array_merge($this->core_js, $this->js) as $script) {
515 4
			if (isset($optimized_includes[$script])) {
516 2
				$optimized_scripts[] = $script;
517
			} else {
518 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
519
			}
520
		}
521 4
		foreach (array_merge($this->core_html, $this->html) as $import) {
522 4
			if (isset($optimized_includes[$import])) {
523 2
				$optimized_imports[] = $import;
524
			} else {
525 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
526
			}
527
		}
528 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
529 4
		$this->Head .= $this->core_config.$this->config;
530 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports);
531 4
	}
532
}
533