You are here

AssetResolver.php in Advanced CSS/JS Aggregation 8.2

File

src/Asset/AssetResolver.php
View source
<?php

namespace Drupal\advagg\Asset;

use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Asset\AssetResolver as CoreAssetResolver;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Asset\LibraryDependencyResolverInterface;
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;
use Drupal\Core\State\StateInterface;

/**
 * The default asset resolver.
 */
class AssetResolver extends CoreAssetResolver implements AssetResolverInterface {

  /**
   * The CSS collection optimizer.
   *
   * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
   */
  protected $cssCollectionOptimizer;

  /**
   * The JS collection optimizer.
   *
   * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
   */
  protected $jsCollectionOptimizer;

  /**
   * The AdvAgg file status state information storage service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $advaggFiles;

  /**
   * 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.
   * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
   *   The CSS collection optimizer.
   * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $js_collection_optimizer
   *   The JS collection optimizer.
   * @param \Drupal\Core\State\StateInterface $advagg_files
   *   The AdvAgg file status state information storage service.
   */
  public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache, AssetCollectionOptimizerInterface $css_collection_optimizer, AssetCollectionOptimizerInterface $js_collection_optimizer, StateInterface $advagg_files) {
    $this->libraryDiscovery = $library_discovery;
    $this->libraryDependencyResolver = $library_dependency_resolver;
    $this->moduleHandler = $module_handler;
    $this->themeManager = $theme_manager;
    $this->languageManager = $language_manager;
    $this->cache = $cache;
    $this->cssCollectionOptimizer = $css_collection_optimizer;
    $this->jsCollectionOptimizer = $js_collection_optimizer;
    $this->advaggFiles = $advagg_files;
  }

  /**
   * {@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_css_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);

    // After alter get file information (in case alter changes things).
    $this->advaggFiles
      ->getMultiple(array_column($css, 'data'));

    // 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 = $this->cssCollectionOptimizer
        ->optimize($css);
    }
    $this->cache
      ->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, [
      'library_info',
    ]);
    return $css;
  }

  /**
   * {@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_js_alter(). Additionally add the current language to support
    // translation of JavaScript files.
    $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);

      // After alter get file information (in case alter changes things).
      $this->advaggFiles
        ->getMultiple(array_column($javascript, 'data'));

      // 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) {
        $js_assets_header = $this->jsCollectionOptimizer
          ->optimize($js_assets_header);
        $js_assets_footer = $this->jsCollectionOptimizer
          ->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,
    ];
  }

}

Classes

Namesort descending Description
AssetResolver The default asset resolver.