Commit fd18ce46 authored by markcarver's avatar markcarver

Issue #3031415 by markcarver: Overhaul CDN Providers API

parent 6aa33213
......@@ -69,18 +69,25 @@ function _drush_bootstrap_generate_docs_settings(Theme $bootstrap) {
$output[] = '```';
// Determine the groups.
$groups = [];
$groups = [
'general' => [],
'components' => [],
'javascript' => [],
'cdn' => [],
'advanced' => [],
];
foreach ($bootstrap->getSettingPlugin() as $setting) {
// Only get the first two groups (we don't need 3rd, or more, levels).
$_groups = array_slice($setting->getGroups(), 0, 2, FALSE);
$_groups = array_filter(array_slice($setting->getGroups(), 0, 2, FALSE));
if (!$_groups) {
continue;
}
$groups[implode(' > ', $_groups)][] = $setting->getPluginDefinition();
$groups[array_keys($_groups)[0]][implode(' > ', $_groups)][] = $setting->getPluginDefinition();
}
// Generate a table of each group's settings.
foreach ($groups as $group => $settings) {
foreach ($groups as $subgroups) {
foreach ($subgroups as $group => $settings) {
$output[] = '';
$output[] = '---';
$output[] = '';
......@@ -112,6 +119,7 @@ function _drush_bootstrap_generate_docs_settings(Theme $bootstrap) {
$output[] = ' </tbody>';
$output[] = '</table>';
}
}
// Ensure we have link references at the bottom.
$output[] = '';
......
......@@ -671,8 +671,8 @@ function _bootstrap_remove_class($class, array &$element, $property = 'attribute
* @endcode
*
* @see \Drupal\bootstrap\Plugin\ProviderManager
* @see \Drupal\bootstrap\Theme::getProviders()
* @see \Drupal\bootstrap\Theme::getProvider()
* @see \Drupal\bootstrap\Theme::getCdnProviders()
* @see \Drupal\bootstrap\Theme::getCdnProvider()
*/
function bootstrap_cdn_provider($provider = NULL, $reset = FALSE) {
Bootstrap::deprecated();
......@@ -746,7 +746,20 @@ function bootstrap_element_smart_description(array &$element, array &$target = N
*
* // After.
* use Drupal\bootstrap\Plugin\ProviderManager;
* $assets = ProviderManager::load($theme, $provider)->getAssets($type);
* $original_type = $type;
* $config = \Drupal::config('system.performance');
* $cdnAssets = ProviderManager::load($theme, $provider)->getCdnAssets();
* $data = [];
* $types = !isset($type) ? ['css', 'js'] : (array) $type;
* foreach ($types as $type) {
* if ($config->get("$type.preprocess") && !empty($cdnAssets['min'][$type])) {
* $data[$type] = $cdnAssets['min'][$type];
* }
* elseif (!empty($data[$type])) {
* $data[$type] = $cdnAssets[$type];
* }
* }
* $assets = is_string($original_type) ? $data[$original_type] : $data;
* @endcode
*
* @see \Drupal\bootstrap\Plugin\Provider\Custom::getAssets()
......@@ -757,7 +770,20 @@ function bootstrap_element_smart_description(array &$element, array &$target = N
*/
function bootstrap_get_cdn_assets($type = NULL, $provider = NULL, $theme = NULL) {
Bootstrap::deprecated();
return ProviderManager::load($theme, $provider)->getAssets($type);
$original_type = $type;
$assets = [];
$config = \Drupal::config('system.performance');
$cdnAssets = ProviderManager::load($theme, $provider)->getCdnAssets();
$types = !isset($type) ? ['css', 'js'] : (array) $type;
foreach ($types as $type) {
if ($config->get("$type.preprocess") && !empty($cdnAssets['min'][$type])) {
$assets[$type] = $cdnAssets['min'][$type];
}
elseif (!empty($data[$type])) {
$assets[$type] = $cdnAssets[$type];
}
}
return is_string($original_type) ? $assets[$original_type] : $assets;
}
/**
......
......@@ -18,12 +18,25 @@ can override CSS, templates, and theme processing.
#### Choose a Starterkit {#starterkit}
- @link sub_theming_cdn CDN Starterkit @endlink - uses the "out-of-the-box"
CSS and JavaScript files served by the [jsDelivr CDN].
CSS and JavaScript files served by a CDN Provider (like [jsDelivr]).
- @link sub_theming_less Less Starterkit @endlink - uses the
[Bootstrap Framework] [Less] source files and a local [Less] preprocessor.
- @link sub_theming_sass Sass Starterkit @endlink - uses the
[Bootstrap Framework] [Sass] source files and a local [Sass] preprocessor.
{.alert.alert-info} **Note** Using the "CDN Starterkit" is the preferred method
for loading Bootstrap CSS and JS on simpler sites that do not use a site-wide
CDN. Using a CDN Provider for loading Bootstrap, however, does mean that it
depends on a third-party service. There is no obligation or commitment made by
this project or these third-party CDN services that guarantees up-time or
quality of service. If you need to customize Bootstrap, you must choose one of
the Less or Sass Starterkits, compile the source code locally, and disable the
"CDN Provider" theme setting. Alternatively, you may also choose to enable a
site-wide CDN implementation for performance reasons.
{.alert.alert-warning} **Warning** All locally compiled versions of Bootstrap
will be superseded by any enabled "CDN Provider"; **do not use both**.
Once you've selected one of the above starterkits, here's how to install it:
1. Copy over one of the starterkits you have chosen from the
......@@ -62,6 +75,6 @@ to customize.
[Drupal Bootstrap]: https://www.drupal.org/project/bootstrap
[Bootstrap Framework]: https://getbootstrap.com/docs/3.4/
[jsDelivr CDN]: http://www.jsdelivr.com
[jsDelivr]: http://www.jsdelivr.com
[Less]: http://lesscss.org
[Sass]: http://sass-lang.com
This diff is collapsed.
......@@ -11,7 +11,7 @@
var $context = $(context);
// General.
$context.find('#edit-general').drupalSetSummary(function () {
$context.find('[data-drupal-selector="edit-general"]').drupalSetSummary(function () {
var summary = [];
// Buttons.
var size = $context.find('select[name="button_size"] :selected');
......@@ -42,7 +42,7 @@
});
// Components.
$context.find('#edit-components').drupalSetSummary(function () {
$context.find('[data-drupal-selector="edit-components"]').drupalSetSummary(function () {
var summary = [];
// Breadcrumbs.
var breadcrumb = parseInt($context.find('select[name="breadcrumb"]').val(), 10);
......@@ -112,7 +112,7 @@
});
});
$context.find('#edit-javascript').drupalSetSummary(function () {
$context.find('[data-drupal-selector="edit-javascript"]').drupalSetSummary(function () {
var summary = [];
if ($context.find('input[name="modal_enabled"]').is(':checked')) {
if ($jQueryUiBridge.is(':checked')) {
......@@ -131,13 +131,13 @@
return summary.join(', ');
});
// Advanced.
$context.find('#edit-advanced').drupalSetSummary(function () {
// CDN.
$context.find('[data-drupal-selector="edit-cdn"]').drupalSetSummary(function () {
var summary = [];
var $cdnProvider = $context.find('select[name="cdn_provider"] :selected');
var cdnProvider = $cdnProvider.val();
if ($cdnProvider.length && cdnProvider.length) {
summary.push(Drupal.t('CDN provider: %provider', { '%provider': $cdnProvider.text() }));
if ($cdnProvider.length) {
summary.push(Drupal.t('Provider: %provider', { '%provider': $cdnProvider.text() }));
// jsDelivr CDN.
if (cdnProvider === 'jsdelivr') {
......@@ -153,6 +153,21 @@
}
return summary.join(', ');
});
// Advanced.
$context.find('[data-drupal-selector="edit-advanced"]').drupalSetSummary(function () {
var summary = [];
var deprecations = [];
if ($context.find('input[name="include_deprecated"]').is(':checked')) {
deprecations.push(Drupal.t('Included'));
}
deprecations.push($context.find('input[name="suppress_deprecated_warnings"]').is(':checked') ? Drupal.t('Warnings Suppressed') : Drupal.t('Warnings Shown'));
summary.push(Drupal.t('Deprecations: @value', {
'@value': deprecations.join(', '),
}));
return summary.join(', ');
});
}
};
......
......@@ -11,8 +11,8 @@ use Drupal\Component\Annotation\Plugin;
*
* @see \Drupal\bootstrap\Plugin\ProviderInterface
* @see \Drupal\bootstrap\Plugin\ProviderManager
* @see \Drupal\bootstrap\Theme::getProviders()
* @see \Drupal\bootstrap\Theme::getProvider()
* @see \Drupal\bootstrap\Theme::getCdnProviders()
* @see \Drupal\bootstrap\Theme::getCdnProvider()
* @see plugin_api
*
* @Annotation
......
......@@ -5,13 +5,19 @@ namespace Drupal\bootstrap;
use Drupal\bootstrap\Plugin\AlterManager;
use Drupal\bootstrap\Plugin\FormManager;
use Drupal\bootstrap\Plugin\PreprocessManager;
use Drupal\bootstrap\Utility\Crypt;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\Component\Utility\Html;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Markup;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* The primary class for the Drupal Bootstrap base theme.
......@@ -92,6 +98,13 @@ class Bootstrap {
*/
const PROJECT_DOCUMENTATION = 'https://drupal-bootstrap.org';
/**
* The project API search URL.
*
* @var string
*/
const PROJECT_API_SEARCH_URL = self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/@query';
/**
* The Drupal Bootstrap project page.
*
......@@ -261,11 +274,13 @@ class Bootstrap {
* @param string $query
* The query to search for.
*
* @return string
* @return \Drupal\Component\Render\FormattableMarkup
* The complete URL to the documentation site.
*/
public static function apiSearchUrl($query = '') {
return self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/' . Html::escape($query);
return new FormattableMarkup(self::PROJECT_API_SEARCH_URL, [
'@query' => $query,
]);
}
/**
......@@ -362,6 +377,7 @@ class Bootstrap {
// Danger class.
t('Delete')->render() => 'danger',
t('Remove')->render() => 'danger',
t('Reset')->render() => 'danger',
t('Uninstall')->render() => 'danger',
// Success class.
......@@ -417,29 +433,40 @@ class Bootstrap {
/**
* Logs and displays a warning about a deprecated function/method being used.
*
* @param string $caller
* Optional. The function or Class::method that should be shown as
* deprecated. If not set, it will be extrapolated automatically from
* the backtrace. This is primarily used when this method is being invoked
* from inside another method that isn't technically deprecated but has to
* support deprecated functionality.
* @param bool $show_message
* Flag indicating whether to show a message to the user. If TRUE, it will
* force showing the message. If FALSE, it will only log the message. If
* not set, showing the message will be determined by whether the current
* theme has suppressed showing deprecated warnings.
*/
public static function deprecated($show_message = NULL) {
public static function deprecated($caller = NULL, $show_message = NULL) {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Extrapolate the caller.
$caller = $backtrace[1];
$class = '';
if (isset($caller['class'])) {
$parts = explode('\\', $caller['class']);
$class = array_pop($parts) . '::';
if (!isset($caller) && !empty($backtrace[1]) && ($info = $backtrace[1])) {
$caller = (!empty($info['class']) ? $info['class'] . '::' : '') . $info['function'];
}
// Remove class namespace.
$method = FALSE;
if (is_string($caller) && strpos($caller, '::') !== FALSE && ($parts = explode('\\', $caller))) {
$method = TRUE;
$caller = array_pop($parts);
}
$message = t('The following function(s) or method(s) have been deprecated, please check the logs for a more detailed backtrace on where these are being invoked. Click on the function or method link to search the documentation site for a possible replacement or solution: <a href=":url" target="_blank">@title</a>', [
':url' => self::apiSearchUrl($class . $caller['function']),
'@title' => ($class ? $caller['class'] . '::' : '') . $caller['function'] . '()',
$message = t('The following @type has been deprecated: <a href=":url" target="_blank">@title</a>. Please check the logs for a more detailed backtrace on where it is being invoked.', [
'@type' => $method ? 'method' : 'function',
':url' => static::apiSearchUrl($caller),
'@title' => $caller,
]);
if ($show_message || (!isset($show_message) && !self::getTheme()->getSetting('suppress_deprecated_warnings', FALSE))) {
if ($show_message || (!isset($show_message) && !static::getTheme()->getSetting('suppress_deprecated_warnings', FALSE))) {
drupal_set_message($message, 'warning');
}
......@@ -686,6 +713,7 @@ class Bootstrap {
t('Cancel')->render() => 'remove',
t('Delete')->render() => 'trash',
t('Remove')->render() => 'trash',
t('Reset')->render() => 'trash',
t('Search')->render() => 'search',
t('Upload')->render() => 'upload',
t('Preview')->render() => 'eye-open',
......@@ -1184,6 +1212,81 @@ class Bootstrap {
}
}
/**
* Retrieves a response from a URI, using cached response if available.
*
* @param string $uri
* The URI to retrieve JSON from.
* @param array $options
* The options to pass to the HTTP client.
* @param \Exception|null $exception
* The exception thrown if there was an error, passed by reference.
*
* @return \Symfony\Component\HttpFoundation\Response
* A Response object.
*/
public static function cachedRequest($uri, array $options = [], &$exception = NULL) {
$options += [
'method' => 'GET',
'headers' => [
'User-Agent' => 'Drupal Bootstrap ' . static::PROJECT_BRANCH . ' (' . static::PROJECT_PAGE . ')',
],
];
$cache = \Drupal::keyValueExpirable('theme:' . static::getTheme()->getName() . ':http');
$key = 'request-' . Crypt::hashBase64(serialize(['uri' => $uri] + $options));
$response = $cache->get($key);
if (!isset($response)) {
/** @var \GuzzleHttp\Client $client */
$client = \Drupal::service('http_client_factory')->fromOptions($options);
$request = new Request($options['method'], $uri, $options['headers']);
try {
$r = $client->send($request, $options);
// In order to actually cache the response, the contents must be
// extracted from the stream before it's stored in the database.
$response = new Response($r->getBody(TRUE)->getContents(), $r->getStatusCode(), $r->getHeaders());
}
catch (GuzzleException $e) {
$exception = $e;
$response = new Response($e->getCode() ?: 500, [], $e->getMessage());
}
catch (\Exception $e) {
$exception = $e;
$response = new Response($e->getCode() ?: 500, [], $e->getMessage());
}
// Only cache if a maximum age has been detected.
if ($response->getStatusCode() == 200 && ($maxAge = $response->getMaxAge())) {
$cache->setWithExpire($key, $response, $maxAge);
}
}
return $response;
}
/**
* Retrieves JSON from a URI.
*
* @param string $uri
* The URI to retrieve JSON from.
* @param array $options
* The options to pass to the HTTP client.
* @param \Exception|null $exception
* The exception thrown if there was an error, passed by reference.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JsonResponse object.
*/
public static function requestJson($uri, array $options = [], &$exception = NULL) {
$r = static::cachedRequest($uri, $options, $exception);
$json = Json::decode($r->getContent() ?: '[]') ?: [];
$response = new JsonResponse($json, $r->getStatusCode(), $r->headers->all());
$response->json = $json;
return $response;
}
/**
* Ensures a value is typecast to a string, rendering an array if necessary.
*
......
......@@ -36,8 +36,8 @@ class LibraryInfo extends PluginBase implements AlterInterface {
}
// Alter the framework library based on currently set CDN provider.
if ($provider = $this->theme->getProvider()) {
$provider->alterFrameworkLibrary($libraries['framework']);
if ($cdnProvider = $this->theme->getCdnProvider()) {
$cdnProvider->alterFrameworkLibrary($libraries['framework']);
}
}
// Core replacements.
......
......@@ -72,6 +72,7 @@ class SystemThemeSettings extends FormBase implements FormInterface {
'general' => t('General'),
'components' => t('Components'),
'javascript' => t('JavaScript'),
'cdn' => t('CDN'),
'advanced' => t('Advanced'),
];
foreach ($groups as $group => $title) {
......@@ -80,9 +81,66 @@ class SystemThemeSettings extends FormBase implements FormInterface {
'#title' => $title,
'#group' => 'bootstrap',
];
// Show a button to reset cached HTTP requests.
if ($group === 'advanced') {
$cache = \Drupal::keyValueExpirable('theme:' . $this->theme->getName() . ':http');
$count = count($cache->getAll());
$form[$group]['reset_http_request_cache'] = [
'#type' => 'item',
'#title' => $this->t('Cached HTTP requests: @count', ['@count' => $count]),
'#weight' => 100,
'#smart_description' => FALSE,
'#description' => $this->t('All HTTP requests initiated through the base-theme are cached if there is a "max-age" response header present. These cached requests will persist through cache rebuilds and only expire once the the "max-age" has been reached. If you believe a CDN Provider is not retrieving data properly, you can manually reset this cache here.'),
'#description_display' => 'before',
'#prefix' => '<div id="reset-http-request-cache">',
'#suffix' => '</div>',
];
$form[$group]['reset_http_request_cache']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Reset HTTP Request Cache'),
'#prefix' => '<div>',
'#suffix' => '</div>',
'#submit' => [
[get_class($this), 'submitResetHttpRequestCache'],
],
'#ajax' => [
'callback' => [get_class($this), 'ajaxResetHttpRequestCache'],
'wrapper' => 'reset-http-request-cache',
],
];
}
}
}
/**
* Submit callback for resetting the cached HTTP requests.
*
* @param array $form
* Nested array of form elements that comprise the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function submitResetHttpRequestCache(array $form, FormStateInterface $form_state) {
$form_state->setRebuild();
$theme = SystemThemeSettings::getTheme(Element::create($form), $form_state);
$cache = \Drupal::keyValueExpirable('theme:' . $theme->getName() . ':http');
$cache->deleteAll();
}
/**
* AJAX callback for reloading the cached HTTP request markup.
*
* @param array $form
* Nested array of form elements that comprise the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function ajaxResetHttpRequestCache(array $form, FormStateInterface $form_state) {
return $form['advanced']['reset_http_request_cache'];
}
/**
* Retrieves the currently selected theme on the settings form.
*
......
......@@ -26,8 +26,8 @@ class Broken extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
public function getAssets($types = NULL) {
return [];
public function getCacheTtl() {
return static::CACHE_TTL;
}
/**
......@@ -37,6 +37,20 @@ class Broken extends PluginBase implements ProviderInterface {
return [];
}
/**
* {@inheritdoc}
*/
public function getCdnExceptions($reset = TRUE) {
return [];
}
/**
* {@inheritdoc}
*/
public function getCdnTheme() {
return NULL;
}
/**
* {@inheritdoc}
*/
......@@ -44,6 +58,13 @@ class Broken extends PluginBase implements ProviderInterface {
return [];
}
/**
* {@inheritdoc}
*/
public function getCdnVersion() {
return NULL;
}
/**
* {@inheritdoc}
*/
......@@ -68,28 +89,40 @@ class Broken extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
public function getCdnTheme() {
return NULL;
public function resetCache() {
// Intentionally left empty.
}
/****************************************************************************
*
* Deprecated methods
*
***************************************************************************/
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function getThemes() {
return [];
public function getApi() {
return NULL;
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function getCdnVersion() {
return NULL;
public function getAssets($types = NULL) {
return [];
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function getVersions() {
public function getThemes() {
return [];
}
......@@ -98,8 +131,8 @@ class Broken extends PluginBase implements ProviderInterface {
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function getApi() {
return NULL;
public function getVersions() {
return [];
}
/**
......
......@@ -39,19 +39,12 @@ class JsDelivr extends ProviderBase {
protected $latestVersion = [];
/**
* A list of themes, keyed by NPM package name.
* A list of themes, keyed by version.
*
* @var array[]
*/
protected $themes = [];
/**
* A list of versions, keyed by NPM package name.
*
* @var array[]
*/
protected $versions = [];
/**
* {@inheritdoc}
*/
......@@ -80,7 +73,7 @@ class JsDelivr extends ProviderBase {
$version = $this->getCdnVersion();
}
if (!isset($this->themes[$version])) {
$this->themes[$version] = $this->cacheGet('themes.' . Unicode::escapeDelimiter($version), [], function ($themes) use ($version) {
$this->themes[$version] = $this->cacheGet('themes', Unicode::escapeDelimiter($version), [], function ($themes) use ($version) {
foreach (['bootstrap', 'bootswatch'] as $package) {
$mappedVersion = $this->mapVersion($version, $package);
$files = $this->requestApiV1($package, $mappedVersion);
......@@ -95,10 +88,10 @@ class JsDelivr extends ProviderBase {
/**
* {@inheritdoc}
*/
public function getCdnVersions($package = 'bootstrap') {
if (!isset($this->versions[$package])) {
$this->versions[$package] = $this->cacheGet("versions.$package", [], function ($versions) use ($package) {
$json = $this->requestApiV1($package) + ['versions' => []];
public function getCdnVersions() {
if (!isset($this->versions)) {
$this->versions = $this->cacheGet('versions', 'bootstrap', [], function ($versions) {
$json = $this->requestApiV1('bootstrap') + ['versions' => []];
foreach ($json['versions'] as $version) {
// Skip irrelevant versions.
if (!preg_match('/^' . substr(Bootstrap::FRAMEWORK_VERSION, 0, 1) . '\.\d+\.\d+$/', $version)) {
......@@ -109,7 +102,7 @@ class JsDelivr extends ProviderBase {
return $versions;
});
}
return $this->versions[$package];
return $this->versions;
}
/**
......@@ -270,30 +263,35 @@ class JsDelivr extends ProviderBase {
* The JSON data from the API.
*/
protected function requestApiV1($package, $version = NULL) {
$url = static::BASE_API_URL . "/$package";
$uri = static::BASE_API_URL . "/$package";
$options = [
// 'collection' => $this->getCacheId(),
];
// If no version was passed, then all versions are returned.
if (!$version) {
return $this->requestJson($url);
$response = Bootstrap::requestJson($uri, $options);
// If bootstrap JSON could not be returned, provide defaults.
if (!$response->json && $this->cdnExceptions && $package === 'bootstrap') {
$response->json = ['versions' => [Bootstrap::FRAMEWORK_VERSION]];
}
return $response->json;
}
$json = $this->requestJson("$url@$version/flat");
$response = Bootstrap::requestJson("$uri@$version/flat", $options);
// If bootstrap JSON could not be returned, provide defaults.
if (!$json && $package === 'bootstrap') {
$version = Bootstrap::FRAMEWORK_VERSION;
if (!$response->json && $this->cdnExceptions && $package === 'bootstrap') {
return [
'css' => [static::BASE_CDN_URL . "/$package@$version/dist/css/bootstrap.css"],
'js' => [static::BASE_CDN_URL . "/$package@$version/dist/js/bootstrap.js"],
'min' => [
'css' => [static::BASE_CDN_URL . "/$package@$version/dist/css/bootstrap.min.css"],
'js' => [static::BASE_CDN_URL . "/$package@$version/dist/js/bootstrap.min.js"],
],
'/dist/css/bootstrap.css',
'/dist/js/bootstrap.js',
'/dist/css/bootstrap.min.css',
'/dist/js/bootstrap.min.js',
];
}
// Parse the files from JSON.
return $this->parseFiles($json);
return $this->parseFiles($response->json);
}
/**
......
This diff is collapsed.
<?php
namespace Drupal\bootstrap\Plugin\Provider;
/**
* Class ProviderException.
*/
class ProviderException extends \RuntimeException {
/**
* The CDN Provider that threw the exception.
*
* @var \Drupal\bootstrap\Plugin\Provider\ProviderInterface
*/
protected $provider;
/**
* ProviderException constructor.
*
* @param \Drupal\bootstrap\Plugin\Provider\ProviderInterface $provider
* The CDN Provider that threw the exception.
* @param string $message
* The exception message.
* @param int $code
* The exception code.
* @param \Throwable $previous
* A previous exception.
*/
public function __construct(ProviderInterface $provider, $message = "", int $code = 0, \Throwable $previous = NULL) {
parent::__construct($message, $code, $previous);