Commit c5942ee0 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2223143 by damiankloip: Consolidate library extension caches into a single cache entry.

parent 580fd45d
Loading
Loading
Loading
Loading
+9 −1
Changes for core/core.services.yml: 9 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -819,7 +819,15 @@ services:
    class: Drupal\Core\Asset\AssetDumper
  library.discovery:
    class: Drupal\Core\Asset\LibraryDiscovery
    arguments: ['@cache.discovery', '@module_handler']
    arguments: ['@library.discovery.collector']
  library.discovery.collector:
    class: Drupal\Core\Asset\LibraryDiscoveryCollector
    arguments: ['@cache.discovery', '@lock', '@library.discovery.parser']
    tags:
      - { name: needs_destruction }
  library.discovery.parser:
    class: Drupal\Core\Asset\LibraryDiscoveryParser
    arguments: ['@module_handler']
  info_parser:
    class: Drupal\Core\Extension\InfoParser
  element_info:
+9 −270
Changes for core/lib/Drupal/Core/Asset/LibraryDiscovery.php: 9 added lines, 270 removed lines.
Original line number Diff line number Diff line
@@ -7,14 +7,7 @@

namespace Drupal\Core\Asset;

use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Cache\CacheCollectorInterface;

/**
 * Discovers available asset libraries in Drupal.
@@ -22,25 +15,11 @@
class LibraryDiscovery implements LibraryDiscoveryInterface {

  /**
   * Stores the library information keyed by extension.
   * The library discovery cache collector.
   *
   * @var array
   * @var \Drupal\Core\Cache\CacheCollectorInterface
   */
  protected $libraries;

  /**
   * The cache backend.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;
  protected $collector;

  /**
   * Constructs a new LibraryDiscovery instance.
@@ -50,263 +29,23 @@ class LibraryDiscovery implements LibraryDiscoveryInterface {
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    $this->cache = $cache_backend;
    $this->moduleHandler = $module_handler;
  public function __construct(CacheCollectorInterface $library_discovery_collector) {
    $this->collector = $library_discovery_collector;
  }

  /**
   * {@inheritdoc}
   */
  public function getLibrariesByExtension($extension) {
    $this->ensureLibraryInformation($extension);
    return $this->libraries[$extension];
    return $this->collector->get($extension);
  }

  /**
   * {@inheritdoc}
   */
  public function getLibraryByName($extension, $name) {
    $this->ensureLibraryInformation($extension);
    return isset($this->libraries[$extension][$name]) ? $this->libraries[$extension][$name] : FALSE;
  }

  /**
   * Ensures that the libraries property is filled.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   */
  protected function ensureLibraryInformation($extension) {
    $this->getCache($extension);
    if (!isset($this->libraries[$extension])) {
      if ($information = $this->buildLibrariesByExtension($extension)) {
        $this->libraries[$extension] = $information;
      }
      else {
        $this->libraries[$extension] = FALSE;
      }
      $this->setCache($extension, $this->libraries[$extension]);
    }
  }

  /**
   * Fills up the libraries property from cache, if available.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   */
  protected function getCache($extension) {
    if (!isset($this->libraries[$extension])) {
      if ($cache = $this->cache->get('library:info:' . $extension)) {
        $this->libraries[$extension] = $cache->data;
      }
    }
  }

  /**
   * Sets the library information into a cache entry.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   *
   * @param bool|array $information
   *   All library definitions of the passed extension or FALSE if no
   *   information is available.
   */
  protected function setCache($extension, $information) {
    $this->cache->set('library:info:' . $extension, $information, Cache::PERMANENT, array(
      'extension' => array(TRUE, $extension),
      'library_info' => array(TRUE),
    ));
  }

  /**
   * Parses and builds up all the libraries information of an extension.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   *
   * @return array
   *   All library definitions of the passed extension.
   *
   * @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException
   *   Thrown when a library has no js/css/setting.
   * @throws \UnexpectedValueException
   *   Thrown when a js file defines a positive weight.
   */
  protected function buildLibrariesByExtension($extension) {
    $this->libraries[$extension] = array();
    if ($extension === 'core') {
      $path = 'core';
      $extension_type = 'core';
    }
    else {
      if ($this->moduleHandler->moduleExists($extension)) {
        $extension_type = 'module';
      }
      else {
        $extension_type = 'theme';
      }
      $path = $this->drupalGetPath($extension_type, $extension);
    }
    $library_file = $path . '/' . $extension . '.libraries.yml';

    if ($library_file && file_exists(DRUPAL_ROOT . '/' . $library_file)) {
      $this->libraries[$extension] = array();
      $this->parseLibraryInfo($extension, $library_file);
    }

    foreach ($this->libraries[$extension] as $id => &$library) {
      if (!isset($library['js']) && !isset($library['css']) && !isset($library['settings'])) {
        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file));
      }
      $library += array('dependencies' => array(), 'js' => array(), 'css' => array());

      if (isset($library['version'])) {
        // @todo Retrieve version of a non-core extension.
        if ($library['version'] === 'VERSION') {
          $library['version'] = \Drupal::VERSION;
        }
        // Remove 'v' prefix from external library versions.
        elseif ($library['version'][0] === 'v') {
          $library['version'] = substr($library['version'], 1);
        }
      }

      foreach (array('js', 'css') as $type) {
        // Prepare (flatten) the SMACSS-categorized definitions.
        // @todo After Asset(ic) changes, retain the definitions as-is and
        //   properly resolve dependencies for all (css) libraries per category,
        //   and only once prior to rendering out an HTML page.
        if ($type == 'css' && !empty($library[$type])) {
          foreach ($library[$type] as $category => $files) {
            foreach ($files as $source => $options) {
              if (!isset($options['weight'])) {
                $options['weight'] = 0;
              }
              // Apply the corresponding weight defined by CSS_* constants.
              $options['weight'] += constant('CSS_' . strtoupper($category));
              $library[$type][$source] = $options;
            }
            unset($library[$type][$category]);
          }
        }
        foreach ($library[$type] as $source => $options) {
          unset($library[$type][$source]);
          // Allow to omit the options hashmap in YAML declarations.
          if (!is_array($options)) {
            $options = array();
          }
          if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) {
            throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library.");
          }
          // Unconditionally apply default groups for the defined asset files.
          // The library system is a dependency management system. Each library
          // properly specifies its dependencies instead of relying on a custom
          // processing order.
          if ($type == 'js') {
            $options['group'] = JS_LIBRARY;
          }
          elseif ($type == 'css') {
            $options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT;
          }
          // By default, all library assets are files.
          if (!isset($options['type'])) {
            $options['type'] = 'file';
          }
          if ($options['type'] == 'external') {
            $options['data'] = $source;
          }
          // Determine the file asset URI.
          else {
            if ($source[0] === '/') {
              // An absolute path maps to DRUPAL_ROOT / base_path().
              if ($source[1] !== '/') {
                $options['data'] = substr($source, 1);
              }
              // A protocol-free URI (e.g., //cdn.com/example.js) is external.
              else {
                $options['type'] = 'external';
                $options['data'] = $source;
              }
            }
            // A stream wrapper URI (e.g., public://generated_js/example.js).
            elseif ($this->fileValidUri($source)) {
              $options['data'] = $source;
            }
            // By default, file paths are relative to the registering extension.
            else {
              $options['data'] = $path . '/' . $source;
            }
          }

          if (!isset($library['version'])) {
            // @todo Get the information from the extension.
            $options['version'] = -1;
          }
          else {
            $options['version'] = $library['version'];
          }

          $library[$type][] = $options;
        }
      }

      // @todo Introduce drupal_add_settings().
      if (isset($library['settings'])) {
        $library['js'][] = array(
          'type' => 'setting',
          'data' => $library['settings'],
        );
        unset($library['settings']);
      }
      // @todo Convert all uses of #attached[library][]=array('provider','name')
      //   into #attached[library][]='provider/name' and remove this.
      foreach ($library['dependencies'] as $i => $dependency) {
        $library['dependencies'][$i] = $dependency;
      }
    }
    return $this->libraries[$extension];
  }

  /**
   * Wraps drupal_get_path().
   */
  protected function drupalGetPath($type, $name) {
    return drupal_get_path($type, $name);
  }

  /**
   * Wraps file_valid_uri().
   */
  protected function fileValidUri($source) {
    return file_valid_uri($source);
  }

  /**
   * Parses a given library file and allows module to alter it.
   *
   * This method sets the parsed information onto the library property.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   * @param string $library_file
   *   The relative filename to the DRUPAL_ROOT of the wanted library file.
   *
   * @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
   *   Thrown when a parser exception got thrown.
   */
  protected function parseLibraryInfo($extension, $library_file) {
    try {
      $this->libraries[$extension] = Yaml::decode(file_get_contents(DRUPAL_ROOT . '/' . $library_file));
    }
    catch (InvalidDataTypeException $e) {
      // Rethrow a more helpful exception to provide context.
      throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
    }
    // Allow modules to alter the module's registered libraries.
    $this->moduleHandler->alter('library_info', $this->libraries[$extension], $extension);
    $extension = $this->getLibrariesByExtension($extension);
    return isset($extension[$name]) ? $extension[$name] : FALSE;
  }

}
+75 −0
Changes for core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php: 75 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains \Drupal\Core\Asset\LibraryDiscoveryCollector.
 */

namespace Drupal\Core\Asset;

use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Lock\LockBackendInterface;

/**
 * A CacheCollector implementation for building library extension info.
 */
class LibraryDiscoveryCollector extends CacheCollector {

  /**
   * The cache key.
   *
   * @var string
   */
  protected $cacheKey = 'library_info';

  /**
   * The cache backend.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The lock backend.
   *
   * @var \Drupal\Core\Lock\LockBackendInterface
   */
  protected $lock;

  /**
   * The library discovery parser.
   *
   * @var \Drupal\Core\Asset\LibraryDiscoveryParser
   */
  protected $discoveryParser;

  /**
   * Constructs a CacheCollector object.
   *
   * @param string $cid
   *   The cid for the array being cached.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend.
   * @param \Drupal\Core\Lock\LockBackendInterface $lock
   *   The lock backend.
   * @param \Drupal\Core\Asset\LibraryDiscoveryParser $discovery_parser
   *   The library discovery parser.
   */
  public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, LibraryDiscoveryParser $discovery_parser) {
    parent::__construct($this->cacheKey, $cache, $lock, array($this->cacheKey => array(TRUE)));

    $this->discoveryParser = $discovery_parser;
  }

  /**
   * {@inheritdoc}
   */
  protected function resolveCacheMiss($key) {
    $this->storage[$key] = $this->discoveryParser->buildByExtension($key);
    $this->persist($key);

    return $this->storage[$key];
  }

}
+231 −0
Changes for core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php: 231 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains \Drupal\Core\Asset\LibraryDiscoveryParser.
 */

namespace Drupal\Core\Asset;

use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;

/**
 * Parses library files to get extension data.
 */
class LibraryDiscoveryParser {

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(ModuleHandlerInterface $module_handler) {
    $this->moduleHandler = $module_handler;
  }

  /**
   * Parses and builds up all the libraries information of an extension.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   *
   * @return array
   *   All library definitions of the passed extension.
   *
   * @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException
   *   Thrown when a library has no js/css/setting.
   * @throws \UnexpectedValueException
   *   Thrown when a js file defines a positive weight.
   */
  public function buildByExtension($extension) {
    $libraries = array();

    if ($extension === 'core') {
      $path = 'core';
      $extension_type = 'core';
    }
    else {
      if ($this->moduleHandler->moduleExists($extension)) {
        $extension_type = 'module';
      }
      else {
        $extension_type = 'theme';
      }
      $path = $this->drupalGetPath($extension_type, $extension);
    }

    $library_file = $path . '/' . $extension . '.libraries.yml';

    if ($library_file && file_exists(DRUPAL_ROOT . '/' . $library_file)) {
      $libraries = $this->parseLibraryInfo($extension, $library_file);
    }

    foreach ($libraries as $id => &$library) {
      if (!isset($library['js']) && !isset($library['css']) && !isset($library['settings'])) {
        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file));
      }
      $library += array('dependencies' => array(), 'js' => array(), 'css' => array());

      if (isset($library['version'])) {
        // @todo Retrieve version of a non-core extension.
        if ($library['version'] === 'VERSION') {
          $library['version'] = \Drupal::VERSION;
        }
        // Remove 'v' prefix from external library versions.
        elseif ($library['version'][0] === 'v') {
          $library['version'] = substr($library['version'], 1);
        }
      }

      foreach (array('js', 'css') as $type) {
        // Prepare (flatten) the SMACSS-categorized definitions.
        // @todo After Asset(ic) changes, retain the definitions as-is and
        //   properly resolve dependencies for all (css) libraries per category,
        //   and only once prior to rendering out an HTML page.
        if ($type == 'css' && !empty($library[$type])) {
          foreach ($library[$type] as $category => $files) {
            foreach ($files as $source => $options) {
              if (!isset($options['weight'])) {
                $options['weight'] = 0;
              }
              // Apply the corresponding weight defined by CSS_* constants.
              $options['weight'] += constant('CSS_' . strtoupper($category));
              $library[$type][$source] = $options;
            }
            unset($library[$type][$category]);
          }
        }
        foreach ($library[$type] as $source => $options) {
          unset($library[$type][$source]);
          // Allow to omit the options hashmap in YAML declarations.
          if (!is_array($options)) {
            $options = array();
          }
          if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) {
            throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library.");
          }
          // Unconditionally apply default groups for the defined asset files.
          // The library system is a dependency management system. Each library
          // properly specifies its dependencies instead of relying on a custom
          // processing order.
          if ($type == 'js') {
            $options['group'] = JS_LIBRARY;
          }
          elseif ($type == 'css') {
            $options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT;
          }
          // By default, all library assets are files.
          if (!isset($options['type'])) {
            $options['type'] = 'file';
          }
          if ($options['type'] == 'external') {
            $options['data'] = $source;
          }
          // Determine the file asset URI.
          else {
            if ($source[0] === '/') {
              // An absolute path maps to DRUPAL_ROOT / base_path().
              if ($source[1] !== '/') {
                $options['data'] = substr($source, 1);
              }
              // A protocol-free URI (e.g., //cdn.com/example.js) is external.
              else {
                $options['type'] = 'external';
                $options['data'] = $source;
              }
            }
            // A stream wrapper URI (e.g., public://generated_js/example.js).
            elseif ($this->fileValidUri($source)) {
              $options['data'] = $source;
            }
            // By default, file paths are relative to the registering extension.
            else {
              $options['data'] = $path . '/' . $source;
            }
          }

          if (!isset($library['version'])) {
            // @todo Get the information from the extension.
            $options['version'] = -1;
          }
          else {
            $options['version'] = $library['version'];
          }

          $library[$type][] = $options;
        }
      }

      // @todo Introduce drupal_add_settings().
      if (isset($library['settings'])) {
        $library['js'][] = array(
          'type' => 'setting',
          'data' => $library['settings'],
        );
        unset($library['settings']);
      }
      // @todo Convert all uses of #attached[library][]=array('provider','name')
      //   into #attached[library][]='provider/name' and remove this.
      foreach ($library['dependencies'] as $i => $dependency) {
        $library['dependencies'][$i] = $dependency;
      }
    }

    return $libraries;
  }

  /**
   * Parses a given library file and allows module to alter it.
   *
   * This method sets the parsed information onto the library property.
   *
   * @param string $extension
   *   The name of the extension that registered a library.
   * @param string $library_file
   *   The relative filename to the DRUPAL_ROOT of the wanted library file.
   *
   * @return array
   *   An array of parsed library data.
   *
   * @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
   *   Thrown when a parser exception got thrown.
   */
  protected function parseLibraryInfo($extension, $library_file) {
    try {
      $libraries = Yaml::decode(file_get_contents(DRUPAL_ROOT . '/' . $library_file));
    }
    catch (InvalidDataTypeException $e) {
      // Rethrow a more helpful exception to provide context.
      throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
    }
    // Allow modules to alter the module's registered libraries.
    $this->moduleHandler->alter('library_info', $libraries, $extension);

    return $libraries;
  }

  /**
   * Wraps drupal_get_path().
   */
  protected function drupalGetPath($type, $name) {
    return drupal_get_path($type, $name);
  }

  /**
   * Wraps file_valid_uri().
   */
  protected function fileValidUri($source) {
    return file_valid_uri($source);
  }

}
+132 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading