Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\Asset;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Component\Utility\Crypt;
|
Chris@0
|
6 use Drupal\Component\Utility\NestedArray;
|
Chris@0
|
7 use Drupal\Core\Cache\CacheBackendInterface;
|
Chris@0
|
8 use Drupal\Core\Extension\ModuleHandlerInterface;
|
Chris@0
|
9 use Drupal\Core\Language\LanguageManagerInterface;
|
Chris@0
|
10 use Drupal\Core\Theme\ThemeManagerInterface;
|
Chris@0
|
11
|
Chris@0
|
12 /**
|
Chris@0
|
13 * The default asset resolver.
|
Chris@0
|
14 */
|
Chris@0
|
15 class AssetResolver implements AssetResolverInterface {
|
Chris@0
|
16
|
Chris@0
|
17 /**
|
Chris@0
|
18 * The library discovery service.
|
Chris@0
|
19 *
|
Chris@0
|
20 * @var \Drupal\Core\Asset\LibraryDiscoveryInterface
|
Chris@0
|
21 */
|
Chris@0
|
22 protected $libraryDiscovery;
|
Chris@0
|
23
|
Chris@0
|
24 /**
|
Chris@0
|
25 * The library dependency resolver.
|
Chris@0
|
26 *
|
Chris@0
|
27 * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
|
Chris@0
|
28 */
|
Chris@0
|
29 protected $libraryDependencyResolver;
|
Chris@0
|
30
|
Chris@0
|
31 /**
|
Chris@0
|
32 * The module handler.
|
Chris@0
|
33 *
|
Chris@0
|
34 * @var \Drupal\Core\Extension\ModuleHandlerInterface
|
Chris@0
|
35 */
|
Chris@0
|
36 protected $moduleHandler;
|
Chris@0
|
37
|
Chris@0
|
38 /**
|
Chris@0
|
39 * The theme manager.
|
Chris@0
|
40 *
|
Chris@0
|
41 * @var \Drupal\Core\Theme\ThemeManagerInterface
|
Chris@0
|
42 */
|
Chris@0
|
43 protected $themeManager;
|
Chris@0
|
44
|
Chris@0
|
45 /**
|
Chris@0
|
46 * The language manager.
|
Chris@0
|
47 *
|
Chris@0
|
48 * @var \Drupal\Core\Language\LanguageManagerInterface
|
Chris@0
|
49 */
|
Chris@0
|
50 protected $languageManager;
|
Chris@0
|
51
|
Chris@0
|
52 /**
|
Chris@0
|
53 * The cache backend.
|
Chris@0
|
54 *
|
Chris@0
|
55 * @var \Drupal\Core\Cache\CacheBackendInterface
|
Chris@0
|
56 */
|
Chris@0
|
57 protected $cache;
|
Chris@0
|
58
|
Chris@0
|
59 /**
|
Chris@0
|
60 * Constructs a new AssetResolver instance.
|
Chris@0
|
61 *
|
Chris@0
|
62 * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
|
Chris@0
|
63 * The library discovery service.
|
Chris@0
|
64 * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver
|
Chris@0
|
65 * The library dependency resolver.
|
Chris@0
|
66 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
Chris@0
|
67 * The module handler.
|
Chris@0
|
68 * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
|
Chris@0
|
69 * The theme manager.
|
Chris@0
|
70 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
Chris@0
|
71 * The language manager.
|
Chris@0
|
72 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
|
Chris@0
|
73 * The cache backend.
|
Chris@0
|
74 */
|
Chris@0
|
75 public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
|
Chris@0
|
76 $this->libraryDiscovery = $library_discovery;
|
Chris@0
|
77 $this->libraryDependencyResolver = $library_dependency_resolver;
|
Chris@0
|
78 $this->moduleHandler = $module_handler;
|
Chris@0
|
79 $this->themeManager = $theme_manager;
|
Chris@0
|
80 $this->languageManager = $language_manager;
|
Chris@0
|
81 $this->cache = $cache;
|
Chris@0
|
82 }
|
Chris@0
|
83
|
Chris@0
|
84 /**
|
Chris@0
|
85 * Returns the libraries that need to be loaded.
|
Chris@0
|
86 *
|
Chris@0
|
87 * For example, with core/a depending on core/c and core/b on core/d:
|
Chris@0
|
88 * @code
|
Chris@0
|
89 * $assets = new AttachedAssets();
|
Chris@0
|
90 * $assets->setLibraries(['core/a', 'core/b', 'core/c']);
|
Chris@0
|
91 * $assets->setAlreadyLoadedLibraries(['core/c']);
|
Chris@0
|
92 * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d']
|
Chris@0
|
93 * @endcode
|
Chris@0
|
94 *
|
Chris@0
|
95 * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
|
Chris@0
|
96 * The assets attached to the current response.
|
Chris@0
|
97 *
|
Chris@0
|
98 * @return string[]
|
Chris@0
|
99 * A list of libraries and their dependencies, in the order they should be
|
Chris@0
|
100 * loaded, excluding any libraries that have already been loaded.
|
Chris@0
|
101 */
|
Chris@0
|
102 protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
|
Chris@0
|
103 return array_diff(
|
Chris@0
|
104 $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
|
Chris@0
|
105 $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
|
Chris@0
|
106 );
|
Chris@0
|
107 }
|
Chris@0
|
108
|
Chris@0
|
109 /**
|
Chris@0
|
110 * {@inheritdoc}
|
Chris@0
|
111 */
|
Chris@0
|
112 public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
|
Chris@0
|
113 $theme_info = $this->themeManager->getActiveTheme();
|
Chris@0
|
114 // Add the theme name to the cache key since themes may implement
|
Chris@0
|
115 // hook_library_info_alter().
|
Chris@0
|
116 $libraries_to_load = $this->getLibrariesToLoad($assets);
|
Chris@0
|
117 $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
|
Chris@0
|
118 if ($cached = $this->cache->get($cid)) {
|
Chris@0
|
119 return $cached->data;
|
Chris@0
|
120 }
|
Chris@0
|
121
|
Chris@0
|
122 $css = [];
|
Chris@0
|
123 $default_options = [
|
Chris@0
|
124 'type' => 'file',
|
Chris@0
|
125 'group' => CSS_AGGREGATE_DEFAULT,
|
Chris@0
|
126 'weight' => 0,
|
Chris@0
|
127 'media' => 'all',
|
Chris@0
|
128 'preprocess' => TRUE,
|
Chris@0
|
129 'browsers' => [],
|
Chris@0
|
130 ];
|
Chris@0
|
131
|
Chris@0
|
132 foreach ($libraries_to_load as $library) {
|
Chris@0
|
133 list($extension, $name) = explode('/', $library, 2);
|
Chris@0
|
134 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
Chris@0
|
135 if (isset($definition['css'])) {
|
Chris@0
|
136 foreach ($definition['css'] as $options) {
|
Chris@0
|
137 $options += $default_options;
|
Chris@0
|
138 $options['browsers'] += [
|
Chris@0
|
139 'IE' => TRUE,
|
Chris@0
|
140 '!IE' => TRUE,
|
Chris@0
|
141 ];
|
Chris@0
|
142
|
Chris@0
|
143 // Files with a query string cannot be preprocessed.
|
Chris@0
|
144 if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
|
Chris@0
|
145 $options['preprocess'] = FALSE;
|
Chris@0
|
146 }
|
Chris@0
|
147
|
Chris@0
|
148 // Always add a tiny value to the weight, to conserve the insertion
|
Chris@0
|
149 // order.
|
Chris@0
|
150 $options['weight'] += count($css) / 1000;
|
Chris@0
|
151
|
Chris@0
|
152 // CSS files are being keyed by the full path.
|
Chris@0
|
153 $css[$options['data']] = $options;
|
Chris@0
|
154 }
|
Chris@0
|
155 }
|
Chris@0
|
156 }
|
Chris@0
|
157
|
Chris@0
|
158 // Allow modules and themes to alter the CSS assets.
|
Chris@0
|
159 $this->moduleHandler->alter('css', $css, $assets);
|
Chris@0
|
160 $this->themeManager->alter('css', $css, $assets);
|
Chris@0
|
161
|
Chris@0
|
162 // Sort CSS items, so that they appear in the correct order.
|
Chris@0
|
163 uasort($css, 'static::sort');
|
Chris@0
|
164
|
Chris@0
|
165 // Allow themes to remove CSS files by CSS files full path and file name.
|
Chris@0
|
166 // @todo Remove in Drupal 9.0.x.
|
Chris@0
|
167 if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
|
Chris@0
|
168 foreach ($css as $key => $options) {
|
Chris@0
|
169 if (isset($stylesheet_remove[$key])) {
|
Chris@0
|
170 unset($css[$key]);
|
Chris@0
|
171 }
|
Chris@0
|
172 }
|
Chris@0
|
173 }
|
Chris@0
|
174
|
Chris@0
|
175 if ($optimize) {
|
Chris@0
|
176 $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
|
Chris@0
|
177 }
|
Chris@0
|
178 $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
|
Chris@0
|
179
|
Chris@0
|
180 return $css;
|
Chris@0
|
181 }
|
Chris@0
|
182
|
Chris@0
|
183 /**
|
Chris@0
|
184 * Returns the JavaScript settings assets for this response's libraries.
|
Chris@0
|
185 *
|
Chris@0
|
186 * Gathers all drupalSettings from all libraries in the attached assets
|
Chris@0
|
187 * collection and merges them.
|
Chris@0
|
188 *
|
Chris@0
|
189 * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
|
Chris@0
|
190 * The assets attached to the current response.
|
Chris@0
|
191 * @return array
|
Chris@0
|
192 * A (possibly optimized) collection of JavaScript assets.
|
Chris@0
|
193 */
|
Chris@0
|
194 protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
|
Chris@0
|
195 $settings = [];
|
Chris@0
|
196
|
Chris@0
|
197 foreach ($this->getLibrariesToLoad($assets) as $library) {
|
Chris@0
|
198 list($extension, $name) = explode('/', $library, 2);
|
Chris@0
|
199 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
Chris@0
|
200 if (isset($definition['drupalSettings'])) {
|
Chris@0
|
201 $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
|
Chris@0
|
202 }
|
Chris@0
|
203 }
|
Chris@0
|
204
|
Chris@0
|
205 return $settings;
|
Chris@0
|
206 }
|
Chris@0
|
207
|
Chris@0
|
208 /**
|
Chris@0
|
209 * {@inheritdoc}
|
Chris@0
|
210 */
|
Chris@0
|
211 public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
|
Chris@0
|
212 $theme_info = $this->themeManager->getActiveTheme();
|
Chris@0
|
213 // Add the theme name to the cache key since themes may implement
|
Chris@0
|
214 // hook_library_info_alter(). Additionally add the current language to
|
Chris@0
|
215 // support translation of JavaScript files via hook_js_alter().
|
Chris@0
|
216 $libraries_to_load = $this->getLibrariesToLoad($assets);
|
Chris@0
|
217 $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
|
Chris@0
|
218
|
Chris@0
|
219 if ($cached = $this->cache->get($cid)) {
|
Chris@0
|
220 list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
|
Chris@0
|
221 }
|
Chris@0
|
222 else {
|
Chris@0
|
223 $javascript = [];
|
Chris@0
|
224 $default_options = [
|
Chris@0
|
225 'type' => 'file',
|
Chris@0
|
226 'group' => JS_DEFAULT,
|
Chris@0
|
227 'weight' => 0,
|
Chris@0
|
228 'cache' => TRUE,
|
Chris@0
|
229 'preprocess' => TRUE,
|
Chris@0
|
230 'attributes' => [],
|
Chris@0
|
231 'version' => NULL,
|
Chris@0
|
232 'browsers' => [],
|
Chris@0
|
233 ];
|
Chris@0
|
234
|
Chris@0
|
235 // Collect all libraries that contain JS assets and are in the header.
|
Chris@0
|
236 $header_js_libraries = [];
|
Chris@0
|
237 foreach ($libraries_to_load as $library) {
|
Chris@0
|
238 list($extension, $name) = explode('/', $library, 2);
|
Chris@0
|
239 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
Chris@0
|
240 if (isset($definition['js']) && !empty($definition['header'])) {
|
Chris@0
|
241 $header_js_libraries[] = $library;
|
Chris@0
|
242 }
|
Chris@0
|
243 }
|
Chris@0
|
244 // The current list of header JS libraries are only those libraries that
|
Chris@0
|
245 // are in the header, but their dependencies must also be loaded for them
|
Chris@0
|
246 // to function correctly, so update the list with those.
|
Chris@0
|
247 $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
|
Chris@0
|
248
|
Chris@0
|
249 foreach ($libraries_to_load as $library) {
|
Chris@0
|
250 list($extension, $name) = explode('/', $library, 2);
|
Chris@0
|
251 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
Chris@0
|
252 if (isset($definition['js'])) {
|
Chris@0
|
253 foreach ($definition['js'] as $options) {
|
Chris@0
|
254 $options += $default_options;
|
Chris@0
|
255
|
Chris@0
|
256 // 'scope' is a calculated option, based on which libraries are
|
Chris@0
|
257 // marked to be loaded from the header (see above).
|
Chris@0
|
258 $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
|
Chris@0
|
259
|
Chris@0
|
260 // Preprocess can only be set if caching is enabled and no
|
Chris@0
|
261 // attributes are set.
|
Chris@0
|
262 $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
|
Chris@0
|
263
|
Chris@0
|
264 // Always add a tiny value to the weight, to conserve the insertion
|
Chris@0
|
265 // order.
|
Chris@0
|
266 $options['weight'] += count($javascript) / 1000;
|
Chris@0
|
267
|
Chris@0
|
268 // Local and external files must keep their name as the associative
|
Chris@0
|
269 // key so the same JavaScript file is not added twice.
|
Chris@0
|
270 $javascript[$options['data']] = $options;
|
Chris@0
|
271 }
|
Chris@0
|
272 }
|
Chris@0
|
273 }
|
Chris@0
|
274
|
Chris@0
|
275 // Allow modules and themes to alter the JavaScript assets.
|
Chris@0
|
276 $this->moduleHandler->alter('js', $javascript, $assets);
|
Chris@0
|
277 $this->themeManager->alter('js', $javascript, $assets);
|
Chris@0
|
278
|
Chris@0
|
279 // Sort JavaScript assets, so that they appear in the correct order.
|
Chris@0
|
280 uasort($javascript, 'static::sort');
|
Chris@0
|
281
|
Chris@0
|
282 // Prepare the return value: filter JavaScript assets per scope.
|
Chris@0
|
283 $js_assets_header = [];
|
Chris@0
|
284 $js_assets_footer = [];
|
Chris@0
|
285 foreach ($javascript as $key => $item) {
|
Chris@0
|
286 if ($item['scope'] == 'header') {
|
Chris@0
|
287 $js_assets_header[$key] = $item;
|
Chris@0
|
288 }
|
Chris@0
|
289 elseif ($item['scope'] == 'footer') {
|
Chris@0
|
290 $js_assets_footer[$key] = $item;
|
Chris@0
|
291 }
|
Chris@0
|
292 }
|
Chris@0
|
293
|
Chris@0
|
294 if ($optimize) {
|
Chris@0
|
295 $collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
|
Chris@0
|
296 $js_assets_header = $collection_optimizer->optimize($js_assets_header);
|
Chris@0
|
297 $js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
|
Chris@0
|
298 }
|
Chris@0
|
299
|
Chris@0
|
300 // If the core/drupalSettings library is being loaded or is already
|
Chris@0
|
301 // loaded, get the JavaScript settings assets, and convert them into a
|
Chris@0
|
302 // single "regular" JavaScript asset.
|
Chris@0
|
303 $libraries_to_load = $this->getLibrariesToLoad($assets);
|
Chris@0
|
304 $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
|
Chris@0
|
305 $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
|
Chris@0
|
306
|
Chris@0
|
307 // Initialize settings to FALSE since they are not needed by default. This
|
Chris@0
|
308 // distinguishes between an empty array which must still allow
|
Chris@0
|
309 // hook_js_settings_alter() to be run.
|
Chris@0
|
310 $settings = FALSE;
|
Chris@0
|
311 if ($settings_required && $settings_have_changed) {
|
Chris@0
|
312 $settings = $this->getJsSettingsAssets($assets);
|
Chris@0
|
313 // Allow modules to add cached JavaScript settings.
|
Chris@0
|
314 foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
|
Chris@18
|
315 $function = $module . '_js_settings_build';
|
Chris@0
|
316 $function($settings, $assets);
|
Chris@0
|
317 }
|
Chris@0
|
318 }
|
Chris@0
|
319 $settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
|
Chris@0
|
320 $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
|
Chris@0
|
321 }
|
Chris@0
|
322
|
Chris@0
|
323 if ($settings !== FALSE) {
|
Chris@0
|
324 // Attached settings override both library definitions and
|
Chris@0
|
325 // hook_js_settings_build().
|
Chris@0
|
326 $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
|
Chris@0
|
327 // Allow modules and themes to alter the JavaScript settings.
|
Chris@0
|
328 $this->moduleHandler->alter('js_settings', $settings, $assets);
|
Chris@0
|
329 $this->themeManager->alter('js_settings', $settings, $assets);
|
Chris@0
|
330 // Update the $assets object accordingly, so that it reflects the final
|
Chris@0
|
331 // settings.
|
Chris@0
|
332 $assets->setSettings($settings);
|
Chris@0
|
333 $settings_as_inline_javascript = [
|
Chris@0
|
334 'type' => 'setting',
|
Chris@0
|
335 'group' => JS_SETTING,
|
Chris@0
|
336 'weight' => 0,
|
Chris@0
|
337 'browsers' => [],
|
Chris@0
|
338 'data' => $settings,
|
Chris@0
|
339 ];
|
Chris@0
|
340 $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
|
Chris@0
|
341 // Prepend to the list of JS assets, to render it first. Preferably in
|
Chris@0
|
342 // the footer, but in the header if necessary.
|
Chris@0
|
343 if ($settings_in_header) {
|
Chris@0
|
344 $js_assets_header = $settings_js_asset + $js_assets_header;
|
Chris@0
|
345 }
|
Chris@0
|
346 else {
|
Chris@0
|
347 $js_assets_footer = $settings_js_asset + $js_assets_footer;
|
Chris@0
|
348 }
|
Chris@0
|
349 }
|
Chris@0
|
350 return [
|
Chris@0
|
351 $js_assets_header,
|
Chris@0
|
352 $js_assets_footer,
|
Chris@0
|
353 ];
|
Chris@0
|
354 }
|
Chris@0
|
355
|
Chris@0
|
356 /**
|
Chris@0
|
357 * Sorts CSS and JavaScript resources.
|
Chris@0
|
358 *
|
Chris@0
|
359 * This sort order helps optimize front-end performance while providing
|
Chris@0
|
360 * modules and themes with the necessary control for ordering the CSS and
|
Chris@0
|
361 * JavaScript appearing on a page.
|
Chris@0
|
362 *
|
Chris@0
|
363 * @param $a
|
Chris@0
|
364 * First item for comparison. The compared items should be associative
|
Chris@0
|
365 * arrays of member items.
|
Chris@0
|
366 * @param $b
|
Chris@0
|
367 * Second item for comparison.
|
Chris@0
|
368 *
|
Chris@0
|
369 * @return int
|
Chris@0
|
370 */
|
Chris@0
|
371 public static function sort($a, $b) {
|
Chris@0
|
372 // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
|
Chris@0
|
373 // group appear before items in the CSS_AGGREGATE_THEME group. Modules may
|
Chris@0
|
374 // create additional groups by defining their own constants.
|
Chris@0
|
375 if ($a['group'] < $b['group']) {
|
Chris@0
|
376 return -1;
|
Chris@0
|
377 }
|
Chris@0
|
378 elseif ($a['group'] > $b['group']) {
|
Chris@0
|
379 return 1;
|
Chris@0
|
380 }
|
Chris@0
|
381 // Finally, order by weight.
|
Chris@0
|
382 elseif ($a['weight'] < $b['weight']) {
|
Chris@0
|
383 return -1;
|
Chris@0
|
384 }
|
Chris@0
|
385 elseif ($a['weight'] > $b['weight']) {
|
Chris@0
|
386 return 1;
|
Chris@0
|
387 }
|
Chris@0
|
388 else {
|
Chris@0
|
389 return 0;
|
Chris@0
|
390 }
|
Chris@0
|
391 }
|
Chris@0
|
392
|
Chris@0
|
393 }
|