You are here

AssetResolver.php in Drupal 8

Namespace

Drupal\Core\Asset

File

core/lib/Drupal/Core/Asset/AssetResolver.php
View source
<?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;
    }
    elseif ($a['weight'] < $b['weight']) {
      return -1;
    }
    elseif ($a['weight'] > $b['weight']) {
      return 1;
    }
    else {
      return 0;
    }
  }

}

Classes

Namesort descending Description
AssetResolver The default asset resolver.