123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- <?php
- namespace Drupal\Core\Asset;
- use Drupal\Component\Utility\Crypt;
- use Drupal\Component\Utility\NestedArray;
- use Drupal\Core\Cache\CacheBackendInterface;
- use Drupal\Core\Extension\ModuleHandlerInterface;
- use Drupal\Core\Language\LanguageManagerInterface;
- use Drupal\Core\Theme\ThemeManagerInterface;
- /**
- * The default asset resolver.
- */
- class AssetResolver implements AssetResolverInterface {
- /**
- * The library discovery service.
- *
- * @var \Drupal\Core\Asset\LibraryDiscoveryInterface
- */
- protected $libraryDiscovery;
- /**
- * The library dependency resolver.
- *
- * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
- */
- protected $libraryDependencyResolver;
- /**
- * The module handler.
- *
- * @var \Drupal\Core\Extension\ModuleHandlerInterface
- */
- protected $moduleHandler;
- /**
- * The theme manager.
- *
- * @var \Drupal\Core\Theme\ThemeManagerInterface
- */
- protected $themeManager;
- /**
- * The language manager.
- *
- * @var \Drupal\Core\Language\LanguageManagerInterface
- */
- protected $languageManager;
- /**
- * The cache backend.
- *
- * @var \Drupal\Core\Cache\CacheBackendInterface
- */
- protected $cache;
- /**
- * Constructs a new AssetResolver instance.
- *
- * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
- * The library discovery service.
- * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver
- * The library dependency resolver.
- * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
- * The module handler.
- * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
- * The theme manager.
- * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
- * The language manager.
- * @param \Drupal\Core\Cache\CacheBackendInterface $cache
- * The cache backend.
- */
- public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
- $this->libraryDiscovery = $library_discovery;
- $this->libraryDependencyResolver = $library_dependency_resolver;
- $this->moduleHandler = $module_handler;
- $this->themeManager = $theme_manager;
- $this->languageManager = $language_manager;
- $this->cache = $cache;
- }
- /**
- * Returns the libraries that need to be loaded.
- *
- * For example, with core/a depending on core/c and core/b on core/d:
- * @code
- * $assets = new AttachedAssets();
- * $assets->setLibraries(['core/a', 'core/b', 'core/c']);
- * $assets->setAlreadyLoadedLibraries(['core/c']);
- * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d']
- * @endcode
- *
- * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
- * The assets attached to the current response.
- *
- * @return string[]
- * A list of libraries and their dependencies, in the order they should be
- * loaded, excluding any libraries that have already been loaded.
- */
- protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
- return array_diff(
- $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
- $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
- );
- }
- /**
- * {@inheritdoc}
- */
- public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
- $theme_info = $this->themeManager->getActiveTheme();
- // Add the theme name to the cache key since themes may implement
- // hook_library_info_alter().
- $libraries_to_load = $this->getLibrariesToLoad($assets);
- $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
- if ($cached = $this->cache->get($cid)) {
- return $cached->data;
- }
- $css = [];
- $default_options = [
- 'type' => 'file',
- 'group' => CSS_AGGREGATE_DEFAULT,
- 'weight' => 0,
- 'media' => 'all',
- 'preprocess' => TRUE,
- 'browsers' => [],
- ];
- foreach ($libraries_to_load as $library) {
- list($extension, $name) = explode('/', $library, 2);
- $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
- if (isset($definition['css'])) {
- foreach ($definition['css'] as $options) {
- $options += $default_options;
- $options['browsers'] += [
- 'IE' => TRUE,
- '!IE' => TRUE,
- ];
- // Files with a query string cannot be preprocessed.
- if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
- $options['preprocess'] = FALSE;
- }
- // Always add a tiny value to the weight, to conserve the insertion
- // order.
- $options['weight'] += count($css) / 1000;
- // CSS files are being keyed by the full path.
- $css[$options['data']] = $options;
- }
- }
- }
- // Allow modules and themes to alter the CSS assets.
- $this->moduleHandler->alter('css', $css, $assets);
- $this->themeManager->alter('css', $css, $assets);
- // Sort CSS items, so that they appear in the correct order.
- uasort($css, 'static::sort');
- // Allow themes to remove CSS files by CSS files full path and file name.
- // @todo Remove in Drupal 9.0.x.
- if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
- foreach ($css as $key => $options) {
- if (isset($stylesheet_remove[$key])) {
- unset($css[$key]);
- }
- }
- }
- if ($optimize) {
- $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
- }
- $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
- return $css;
- }
- /**
- * Returns the JavaScript settings assets for this response's libraries.
- *
- * Gathers all drupalSettings from all libraries in the attached assets
- * collection and merges them.
- *
- * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
- * The assets attached to the current response.
- * @return array
- * A (possibly optimized) collection of JavaScript assets.
- */
- protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
- $settings = [];
- foreach ($this->getLibrariesToLoad($assets) as $library) {
- list($extension, $name) = explode('/', $library, 2);
- $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
- if (isset($definition['drupalSettings'])) {
- $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
- }
- }
- return $settings;
- }
- /**
- * {@inheritdoc}
- */
- public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
- $theme_info = $this->themeManager->getActiveTheme();
- // Add the theme name to the cache key since themes may implement
- // hook_library_info_alter(). Additionally add the current language to
- // support translation of JavaScript files via hook_js_alter().
- $libraries_to_load = $this->getLibrariesToLoad($assets);
- $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
- if ($cached = $this->cache->get($cid)) {
- list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
- }
- else {
- $javascript = [];
- $default_options = [
- 'type' => 'file',
- 'group' => JS_DEFAULT,
- 'weight' => 0,
- 'cache' => TRUE,
- 'preprocess' => TRUE,
- 'attributes' => [],
- 'version' => NULL,
- 'browsers' => [],
- ];
- // Collect all libraries that contain JS assets and are in the header.
- $header_js_libraries = [];
- foreach ($libraries_to_load as $library) {
- list($extension, $name) = explode('/', $library, 2);
- $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
- if (isset($definition['js']) && !empty($definition['header'])) {
- $header_js_libraries[] = $library;
- }
- }
- // The current list of header JS libraries are only those libraries that
- // are in the header, but their dependencies must also be loaded for them
- // to function correctly, so update the list with those.
- $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
- foreach ($libraries_to_load as $library) {
- list($extension, $name) = explode('/', $library, 2);
- $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
- if (isset($definition['js'])) {
- foreach ($definition['js'] as $options) {
- $options += $default_options;
- // 'scope' is a calculated option, based on which libraries are
- // marked to be loaded from the header (see above).
- $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
- // Preprocess can only be set if caching is enabled and no
- // attributes are set.
- $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
- // Always add a tiny value to the weight, to conserve the insertion
- // order.
- $options['weight'] += count($javascript) / 1000;
- // Local and external files must keep their name as the associative
- // key so the same JavaScript file is not added twice.
- $javascript[$options['data']] = $options;
- }
- }
- }
- // Allow modules and themes to alter the JavaScript assets.
- $this->moduleHandler->alter('js', $javascript, $assets);
- $this->themeManager->alter('js', $javascript, $assets);
- // Sort JavaScript assets, so that they appear in the correct order.
- uasort($javascript, 'static::sort');
- // Prepare the return value: filter JavaScript assets per scope.
- $js_assets_header = [];
- $js_assets_footer = [];
- foreach ($javascript as $key => $item) {
- if ($item['scope'] == 'header') {
- $js_assets_header[$key] = $item;
- }
- elseif ($item['scope'] == 'footer') {
- $js_assets_footer[$key] = $item;
- }
- }
- if ($optimize) {
- $collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
- $js_assets_header = $collection_optimizer->optimize($js_assets_header);
- $js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
- }
- // If the core/drupalSettings library is being loaded or is already
- // loaded, get the JavaScript settings assets, and convert them into a
- // single "regular" JavaScript asset.
- $libraries_to_load = $this->getLibrariesToLoad($assets);
- $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
- $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
- // Initialize settings to FALSE since they are not needed by default. This
- // distinguishes between an empty array which must still allow
- // hook_js_settings_alter() to be run.
- $settings = FALSE;
- if ($settings_required && $settings_have_changed) {
- $settings = $this->getJsSettingsAssets($assets);
- // Allow modules to add cached JavaScript settings.
- foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
- $function = $module . '_' . 'js_settings_build';
- $function($settings, $assets);
- }
- }
- $settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
- $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
- }
- if ($settings !== FALSE) {
- // Attached settings override both library definitions and
- // hook_js_settings_build().
- $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
- // Allow modules and themes to alter the JavaScript settings.
- $this->moduleHandler->alter('js_settings', $settings, $assets);
- $this->themeManager->alter('js_settings', $settings, $assets);
- // Update the $assets object accordingly, so that it reflects the final
- // settings.
- $assets->setSettings($settings);
- $settings_as_inline_javascript = [
- 'type' => 'setting',
- 'group' => JS_SETTING,
- 'weight' => 0,
- 'browsers' => [],
- 'data' => $settings,
- ];
- $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
- // Prepend to the list of JS assets, to render it first. Preferably in
- // the footer, but in the header if necessary.
- if ($settings_in_header) {
- $js_assets_header = $settings_js_asset + $js_assets_header;
- }
- else {
- $js_assets_footer = $settings_js_asset + $js_assets_footer;
- }
- }
- return [
- $js_assets_header,
- $js_assets_footer,
- ];
- }
- /**
- * Sorts CSS and JavaScript resources.
- *
- * This sort order helps optimize front-end performance while providing
- * modules and themes with the necessary control for ordering the CSS and
- * JavaScript appearing on a page.
- *
- * @param $a
- * First item for comparison. The compared items should be associative
- * arrays of member items.
- * @param $b
- * Second item for comparison.
- *
- * @return int
- */
- public static function sort($a, $b) {
- // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
- // group appear before items in the CSS_AGGREGATE_THEME group. Modules may
- // create additional groups by defining their own constants.
- if ($a['group'] < $b['group']) {
- return -1;
- }
- elseif ($a['group'] > $b['group']) {
- return 1;
- }
- // Finally, order by weight.
- elseif ($a['weight'] < $b['weight']) {
- return -1;
- }
- elseif ($a['weight'] > $b['weight']) {
- return 1;
- }
- else {
- return 0;
- }
- }
- }
|