comparison core/lib/Drupal/Core/Asset/AssetResolver.php @ 0:c75dbcec494b

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