Completed
Push — master ( 5b91fe...d7aa39 )
by Nazar
07:05
created

Assets::css()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

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

379
		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

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

453
		$Response->/** @scrutinizer ignore-call */ 
454
             cookie('pushed', 1, 0, true);
Loading history...
454 6
		foreach (array_unique($preload) as $resource) {
455 6
			$extension = explode('?', file_extension($resource))[0];
456 6
			$as        = $this->extension_to_as[$extension];
457 6
			$resource  = str_replace(' ', '%20', $resource);
458 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

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