Completed
Push — master ( 48a890...1e64fc )
by Nazar
04:00
created

Includes   C

Complexity

Total Complexity 75

Size/Duplication

Total Lines 567
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11
Metric Value
dl 0
loc 567
rs 5.4482
wmc 75
lcom 1
cbo 11

26 Methods

Rating   Name   Duplication   Size   Complexity  
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 init_includes() 0 11 1
A html() 0 3 1
A html_internal() 0 3 1
A js() 0 3 1
A js_internal() 0 3 1
A css() 0 3 1
A css_internal() 0 3 1
B include_common() 0 21 7
A config() 0 3 1
A config_internal() 0 16 2
B add_includes_on_page() 0 38 5
A ie_edge() 0 10 2
B add_system_configs() 0 29 3
A webcomponents_polyfill() 0 18 4
A get_includes_and_preload_resource_for_page_with_compression() 0 16 3
A rebuild_cache() 0 19 3
A rebuild_cache_optimized() 0 13 3
C get_normalized_includes() 0 39 7
B get_dependency_component() 0 12 7
A add_includes_on_page_manually_added() 0 16 4
B add_includes_on_page_manually_added_normal() 0 26 3
A add_preload() 0 9 2
C add_includes_on_page_manually_added_frontend_load_optimization() 0 35 7

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