Assets::init_assets()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
ccs 9
cts 9
cp 1
crap 1
1
<?php
2
/**
3
 * @package CleverStyle Framework
4
 * @author  Nazar Mokrynskyi <[email protected]>
5
 * @license 0BSD
6
 */
7
namespace cs\Page;
8
use
9
	cs\App,
10
	cs\Config,
11
	cs\Language,
12
	cs\Request,
13
	cs\Response,
14
	h,
15
	cs\Page\Assets\Cache,
16
	cs\Page\Assets\Collecting,
17
	cs\Page\Assets\RequireJS;
18
19
/**
20
 * Assets management for `cs\Page` class
21
 *
22
 * @property string $Title
23
 * @property string $Description
24
 * @property string $canonical_url
25
 * @property string $Head
26
 * @property string $post_Body
27
 * @property string $theme
28
 */
29
trait Assets {
30
	protected $extension_to_as = [
31
		'jpeg'  => 'image',
32
		'jpe'   => 'image',
33
		'jpg'   => 'image',
34
		'gif'   => 'image',
35
		'png'   => 'image',
36
		'svg'   => 'image',
37
		'svgz'  => 'image',
38
		'woff2' => 'font',
39
		'css'   => 'style',
40
		'js'    => 'script',
41
		'html'  => 'document'
42
	];
43
	/**
44
	 * @var array
45
	 */
46
	protected $core_html;
47
	/**
48
	 * @var array
49
	 */
50
	protected $core_js;
51
	/**
52
	 * @var array
53
	 */
54
	protected $core_css;
55
	/**
56
	 * @var string
57
	 */
58
	protected $core_config;
59
	/**
60
	 * @var array
61
	 */
62
	protected $html;
63
	/**
64
	 * @var array
65
	 */
66
	protected $js;
67
	/**
68
	 * @var array
69
	 */
70
	protected $css;
71
	/**
72
	 * @var string
73
	 */
74
	protected $config;
75 54
	protected function init_assets () {
76 54
		$this->core_html   = [];
77 54
		$this->core_js     = [];
78 54
		$this->core_css    = [];
79 54
		$this->core_config = '';
80 54
		$this->html        = [];
81 54
		$this->js          = [];
82 54
		$this->css         = [];
83 54
		$this->config      = '';
84 54
	}
85
	/**
86
	 * @param string|string[] $add
87
	 *
88
	 * @return \cs\Page
89
	 */
90 6
	protected function core_html ($add) {
91 6
		return $this->include_common('html', $add, true);
92
	}
93
	/**
94
	 * @param string|string[] $add
95
	 *
96
	 * @return \cs\Page
97
	 */
98 6
	protected function core_js ($add) {
99 6
		return $this->include_common('js', $add, true);
100
	}
101
	/**
102
	 * @param string|string[] $add
103
	 *
104
	 * @return \cs\Page
105
	 */
106 6
	protected function core_css ($add) {
107 6
		return $this->include_common('css', $add, true);
108
	}
109
	/**
110
	 * Including of Web Components
111
	 *
112
	 * @param string|string[] $add Path to including file, or code
113
	 *
114
	 * @return \cs\Page
115
	 */
116 3
	public function html ($add) {
117 3
		return $this->include_common('html', $add, false);
118
	}
119
	/**
120
	 * Including of JavaScript
121
	 *
122
	 * @param string|string[] $add Path to including file, or code
123
	 *
124
	 * @return \cs\Page
125
	 */
126 3
	public function js ($add) {
127 3
		return $this->include_common('js', $add, false);
128
	}
129
	/**
130
	 * Including of CSS
131
	 *
132
	 * @param string|string[] $add Path to including file, or code
133
	 *
134
	 * @return \cs\Page
135
	 */
136 3
	public function css ($add) {
137 3
		return $this->include_common('css', $add, false);
138
	}
139
	/**
140
	 * @param string          $what
141
	 * @param string|string[] $add
142
	 * @param bool            $core
143
	 *
144
	 * @return \cs\Page
145
	 */
146 6
	protected function include_common ($what, $add, $core) {
147 6
		if (!$add) {
148 3
			return $this;
149
		}
150 6
		if (is_array($add)) {
151 6
			foreach (array_filter($add) as $a) {
152 6
				$this->include_common($what, $a, $core);
153
			}
154
		} else {
155 6
			if ($core) {
156 6
				$what = "core_$what";
157
			}
158 6
			$target   = &$this->$what;
159 6
			$target[] = $add;
160
		}
161 6
		return $this;
162
	}
163
	/**
164
	 * Add config on page to make it available on frontend
165
	 *
166
	 * @param mixed  $config_structure        Any scalar type or array
167
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
168
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
169
	 *
170
	 * @return \cs\Page
171
	 */
172 6
	public function config ($config_structure, $target) {
173 6
		return $this->config_internal($config_structure, $target);
174
	}
175
	/**
176
	 * @param mixed  $config_structure
177
	 * @param string $target
178
	 * @param bool   $core
179
	 *
180
	 * @return \cs\Page
181
	 */
182 6
	protected function config_internal ($config_structure, $target, $core = false) {
183 6
		$config = h::script(
0 ignored issues
show
Bug introduced by
The method script() does not exist on h. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
		/** @scrutinizer ignore-call */ 
184
  $config = h::script(
Loading history...
184 6
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
185
			[
186 6
				'target' => $target,
187 6
				'class'  => 'cs-config',
188 6
				'type'   => 'application/json'
189
			]
190
		);
191 6
		if ($core) {
192 6
			$this->core_config .= $config;
193
		} else {
194 6
			$this->config .= $config;
195
		}
196 6
		return $this;
197
	}
198
	/**
199
	 * Getting of HTML, JS and CSS assets
200
	 *
201
	 * @return \cs\Page
202
	 */
203 6
	protected function add_assets_on_page () {
204 6
		$Config = Config::instance(true);
205 6
		if (!$Config) {
206 3
			return $this;
207
		}
208 6
		$Request = Request::instance();
209
		/**
210
		 * If CSS and JavaScript compression enabled
211
		 */
212 6
		$L = Language::instance();
213 6
		if ($this->page_compression_usage($Config, $Request)) {
214
			/**
215
			 * Rebuilding HTML, JS and CSS cache if necessary
216
			 */
217 6
			(new Cache)->rebuild($Config, $L, $this->theme);
218 6
			$this->webcomponents_polyfill($Request, $Config, true);
219 6
			$languages_hash = md5(implode('', $Config->core['active_languages']));
220 6
			$language_hash  = file_get_json(PUBLIC_CACHE."/languages-$languages_hash.json")[$L->clanguage];
221 6
			$this->config_internal(
222
				[
223 6
					'language' => $L->clanguage,
224 6
					'hash'     => $language_hash
225
				],
226 6
				'cs.current_language',
227 6
				true
228
			);
229 6
			list($assets, $preload) = $this->get_assets_and_preload_resource_for_page_with_compression($Request);
230
		} else {
231 3
			$this->webcomponents_polyfill($Request, $Config, false);
232
			/**
233
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
234
			 */
235 3
			$this->config_internal($L, 'cs.Language', true);
236 3
			$this->config_internal(RequireJS::get_config(), 'requirejs', true);
237 3
			$assets  = $this->get_assets_for_page_without_compression($Config, $Request);
238 3
			$preload = [];
239
		}
240 6
		$this->core_css($assets['css']);
241 6
		$this->core_js($assets['js']);
242 6
		if (isset($assets['html'])) {
243 6
			$this->core_html($assets['html']);
244
		}
245 6
		$this->add_assets_on_page_manually_added($Config, $Request, $preload);
246 6
		return $this;
247
	}
248
	/**
249
	 * @param Config  $Config
250
	 * @param Request $Request
251
	 *
252
	 * @return bool
253
	 */
254 6
	protected function page_compression_usage ($Config, $Request) {
255 6
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
256
	}
257
	/**
258
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
259
	 *
260
	 * @param Request $Request
261
	 * @param Config  $Config
262
	 * @param bool    $with_compression
263
	 */
264 6
	protected function webcomponents_polyfill ($Request, $Config, $with_compression) {
265 6
		if (($this->theme != Config::SYSTEM_THEME && $Config->core['disable_webcomponents']) || $Request->cookie('shadow_dom_v1') == 1) {
266 3
			return;
267
		}
268 6
		if ($with_compression) {
269 6
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
270 6
			$this->add_script_imports_to_document($Config, "<script src=\"/storage/public_cache/$hash.js\"></script>\n");
271
		} else {
272 3
			$this->add_script_imports_to_document($Config, "<script src=\"/assets/js/WebComponents-polyfill/webcomponents-hi-sd-ce.min.js\"></script>\n");
273
		}
274 6
	}
275
	/**
276
	 * @param Config $Config
277
	 * @param string $content
278
	 */
279 6
	protected function add_script_imports_to_document ($Config, $content) {
280 6
		if ($Config->core['put_js_after_body']) {
281 6
			$this->post_Body .= $content;
282
		} else {
283 3
			$this->Head .= $content;
284
		}
285 6
	}
286
	/**
287
	 * @param Request $Request
288
	 *
289
	 * @return array[]
290
	 */
291 6
	protected function get_assets_and_preload_resource_for_page_with_compression ($Request) {
292 6
		list($dependencies, $compressed_assets_map, $not_embedded_resources_map) = file_get_json(PUBLIC_CACHE."/$this->theme.json");
293 6
		$assets  = $this->get_normalized_assets($dependencies, $compressed_assets_map, $Request);
294 6
		$preload = [];
295 6
		foreach (array_merge(...array_values($assets)) as $path) {
0 ignored issues
show
Bug introduced by
array_values($assets) is expanded, but the parameter $array1 of array_merge() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

295
		foreach (array_merge(/** @scrutinizer ignore-type */ ...array_values($assets)) as $path) {
Loading history...
296 6
			$preload[] = [$path];
297 6
			if (isset($not_embedded_resources_map[$path])) {
298 6
				$preload[] = $not_embedded_resources_map[$path];
299
			}
300
		}
301 6
		return [$assets, array_merge(...$preload)];
302
	}
303
	/**
304
	 * @param array      $dependencies
305
	 * @param string[][] $assets_map
306
	 * @param Request    $Request
307
	 *
308
	 * @return string[][]
309
	 */
310 6
	protected function get_normalized_assets ($dependencies, $assets_map, $Request) {
311 6
		$current_module = $Request->current_module;
312
		/**
313
		 * Current URL based on controller path (it better represents how page was rendered)
314
		 */
315 6
		$current_url = array_slice(App::instance()->controller_path, 1);
316 6
		$current_url = ($Request->admin_path ? 'admin/' : '')."$current_module/".implode('/', $current_url);
317
		/**
318
		 * Narrow the dependencies to current module only
319
		 */
320 6
		$dependencies  = array_unique(
321 6
			array_merge(
322 6
				['System'],
323 6
				$dependencies['System'],
324 6
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
325
			)
326
		);
327 6
		$system_assets = [];
328
		// Array with empty array in order to avoid `array_merge()` failure later
329 6
		$dependencies_assets = array_fill_keys($dependencies, [[]]);
330 6
		$assets              = [];
331 6
		foreach ($assets_map as $path => $local_assets) {
332 6
			if ($path == 'System') {
333 6
				$system_assets = $local_assets;
334 6
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
335
				/**
336
				 * @var string $component
337
				 */
338 3
				$dependencies_assets[$component][] = $local_assets;
339 6
			} elseif (mb_strpos($current_url, $path) === 0) {
340 6
				$assets[] = $local_assets;
341
			}
342
		}
343
		// Convert to indexed array first
344 6
		$dependencies_assets = array_values($dependencies_assets);
345
		// Flatten array on higher level
346 6
		$dependencies_assets = array_merge(...$dependencies_assets);
0 ignored issues
show
Bug introduced by
$dependencies_assets is expanded, but the parameter $array1 of array_merge() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

346
		$dependencies_assets = array_merge(/** @scrutinizer ignore-type */ ...$dependencies_assets);
Loading history...
347 6
		return _array(array_merge_recursive($system_assets, ...$dependencies_assets, ...$assets));
348
	}
349
	/**
350
	 * @param array   $dependencies
351
	 * @param string  $url
352
	 * @param Request $Request
353
	 *
354
	 * @return false|string
355
	 */
356 6
	protected function get_dependency_component ($dependencies, $url, $Request) {
357 6
		$url_exploded = explode('/', $url);
358
		/** @noinspection NestedTernaryOperatorInspection */
359 6
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
360
		$is_dependency =
361 6
			$url_component !== Config::SYSTEM_MODULE &&
362 6
			in_array($url_component, $dependencies) &&
363
			(
364 6
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
365
			);
366 6
		return $is_dependency ? $url_component : false;
367
	}
368
	/**
369
	 * @param Config  $Config
370
	 * @param Request $Request
371
	 *
372
	 * @return string[][]
373
	 */
374 3
	protected function get_assets_for_page_without_compression ($Config, $Request) {
375
		// To determine all dependencies and stuff we need `$Config` object to be already created
376 3
		list($dependencies, $assets_map) = Collecting::get_assets_dependencies_and_map($Config, $this->theme);
377 3
		$assets = $this->get_normalized_assets($dependencies, $assets_map, $Request);
378 3
		return $this->add_versions_hash(_substr($assets, strlen(DIR)));
0 ignored issues
show
Bug introduced by
$assets of type array<mixed,string[]> is incompatible with the type string|string[] expected by parameter $string of _substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

378
		return $this->add_versions_hash(_substr(/** @scrutinizer ignore-type */ $assets, strlen(DIR)));
Loading history...
Bug introduced by
_substr($assets, strlen(cs\Page\DIR)) of type string|string[] is incompatible with the type array<mixed,string[]> expected by parameter $assets of cs\Page\Assets::add_versions_hash(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

378
		return $this->add_versions_hash(/** @scrutinizer ignore-type */ _substr($assets, strlen(DIR)));
Loading history...
379
	}
380
	/**
381
	 * @param string[][] $assets
382
	 *
383
	 * @return string[][]
384
	 */
385 3
	protected function add_versions_hash ($assets) {
386 3
		$content      = array_reduce(
387 3
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
388 3
			function ($content, $file) {
389
				return $content.file_get_contents($file);
390 3
			}
391
		);
392 3
		$content_hash = substr(md5($content), 0, 5);
393 3
		foreach ($assets as &$files) {
394 3
			foreach ($files as &$file) {
395 3
				$file .= "?$content_hash";
396
			}
397 3
			unset($file);
398
		}
399 3
		return $assets;
400
	}
401
	/**
402
	 * @param Config   $Config
403
	 * @param Request  $Request
404
	 * @param string[] $preload
405
	 */
406 6
	protected function add_assets_on_page_manually_added ($Config, $Request, $preload) {
407
		/** @noinspection NestedTernaryOperatorInspection */
408 6
		$this->Head .= array_reduce(
409 6
			array_merge($this->core_css, $this->css),
410 6
			function ($content, $href) {
411 6
				return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
412 6
			}
413
		);
414 6
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
415 6
			$this->add_assets_on_page_manually_added_frontend_load_optimization($Config, $Request);
416
		} else {
417 3
			$this->add_assets_on_page_manually_added_normal($Config, $Request, $preload);
418
		}
419 6
	}
420
	/**
421
	 * @param Config   $Config
422
	 * @param Request  $Request
423
	 * @param string[] $preload
424
	 */
425 3
	protected function add_assets_on_page_manually_added_normal ($Config, $Request, $preload) {
426 3
		$this->add_preload($preload, $Request);
427 3
		$configs      = $this->core_config.$this->config;
428 3
		$scripts      = array_reduce(
429 3
			array_merge($this->core_js, $this->js),
430 3
			function ($content, $src) {
431 3
				return "$content<script src=\"$src\"></script>\n";
432 3
			}
433
		);
434 3
		$html_imports = array_reduce(
435 3
			array_merge($this->core_html, $this->html),
436 3
			function ($content, $href) {
437 3
				return "$content<link href=\"$href\" rel=\"import\">\n";
438 3
			}
439
		);
440 3
		$this->Head   .= $configs;
441 3
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
442 3
	}
443
	/**
444
	 * @param string[] $preload
445
	 * @param Request  $Request
446
	 */
447 6
	protected function add_preload ($preload, $Request) {
448 6
		if ($Request->cookie('pushed')) {
449 3
			return;
450
		}
451 6
		$Response = Response::instance();
452 6
		$Response->cookie('pushed', 1, 0, true);
0 ignored issues
show
Bug introduced by
The method cookie() does not exist on cs\False_class. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

452
		$Response->/** @scrutinizer ignore-call */ 
453
             cookie('pushed', 1, 0, true);
Loading history...
453 6
		foreach (array_unique($preload) as $resource) {
454 6
			$extension = explode('?', file_extension($resource))[0];
455 6
			$as        = $this->extension_to_as[$extension];
456 6
			$resource  = str_replace(' ', '%20', $resource);
457 6
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
0 ignored issues
show
Bug introduced by
The method header() does not exist on cs\False_class. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

457
			$Response->/** @scrutinizer ignore-call */ 
458
              header('Link', "<$resource>; rel=preload; as=$as", false);
Loading history...
458
		}
459 6
	}
460
	/**
461
	 * @param Config  $Config
462
	 * @param Request $Request
463
	 */
464 6
	protected function add_assets_on_page_manually_added_frontend_load_optimization ($Config, $Request) {
465 6
		list($optimized_assets, $preload) = file_get_json(PUBLIC_CACHE."/$this->theme.optimized.json");
466 6
		$this->add_preload(
467 6
			array_merge($preload, $this->core_css, $this->css),
468 6
			$Request
469
		);
470 6
		$optimized_assets  = array_flip($optimized_assets);
471 6
		$system_scripts    = '';
472 6
		$optimized_scripts = [];
473 6
		$system_imports    = '';
474 6
		$optimized_imports = [];
475 6
		foreach (array_merge($this->core_js, $this->js) as $script) {
476 6
			if (isset($optimized_assets[$script])) {
477 3
				$optimized_scripts[] = $script;
478
			} else {
479 6
				$system_scripts .= "<script src=\"$script\"></script>\n";
480
			}
481
		}
482 6
		foreach (array_merge($this->core_html, $this->html) as $import) {
483 6
			if (isset($optimized_assets[$import])) {
484 3
				$optimized_imports[] = $import;
485
			} else {
486 6
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
487
			}
488
		}
489 6
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_assets');
490 6
		$this->Head .= $this->core_config.$this->config;
491 6
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports);
492 6
	}
493
}
494